Tool runtime and security architecture
This page is the architecture analysis for the tools/integrations/security module. It complements the implementation pages by focusing on module boundary, capability injection seams, and the trust pipeline rather than re-listing every tool name or permission string.
Scope: from a model-visible tool schema to either an executed action or a structured denial. Implementation specifics live in Tool runtime, events, and integration flows, Built-in tools and permissions, MCP, plugins, and hooks, and Settings, policy, and integrations.
Module purpose
This module owns the action side of the agent loop. It decides what capabilities exist, what becomes model-visible, who is allowed to invoke them, and how those decisions are propagated to SDK and remote hosts.
It deliberately combines three concerns that cannot be separated in practice:
- Tool catalog (built-in, MCP, plugin, external, skill, task tools).
- Trust pipeline (visibility filter → permission rules → hooks → tool-specific guards).
- Integration surface (MCP, plugins, IDE/Chrome/file, hooks, SDK, Remote Control).
Architecture thesis
The capability plane is built on a capability registry plus a single execution boundary:
- Different sources (built-ins, MCP, plugins, skills, tasks, external definitions) all contribute through the same registry shape.
- Every tool call passes through
ToolExecutionBoundary, a mediated execution function that combines schema validation, hooks, permission decisions, host control requests, and tool-specific guards before invoking the underlying tool body.
This design makes adding a capability source cheap and adding a security control safe.
Source anchors
| Semantic alias | String or symbol | Architectural meaning |
|---|---|---|
| BuiltInToolNameConstant | var Rq="Bash" | Built-in tool name constant; representative of the catalog shape. |
| CapabilityConstantGroup | TaskCreate, TaskGet, TaskList, TaskUpdate, Skill, TodoWrite | Capability constants grouped with skill/task tools. |
| ToolExecutionBoundary | async function Yny | Main tool-execution boundary; initial validation → hooks → permission decision → final replacement validation → execute. |
| ToolCallBlockPlanner | Y3g, J3g, X3g | Partitions one model response into contiguous concurrency-safe blocks and singleton ordering barriers. |
| ToolConcurrencyCap | K3g, CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY | Positive environment override, otherwise a default parallel-block cap of 10. |
| ToolUseRejectedTelemetry | tengu_tool_use_can_use_tool_rejected | Denial telemetry inside the execution boundary. |
| ToolUseAllowedTelemetry | tengu_tool_use_can_use_tool_allowed | Allow telemetry inside the same boundary. |
| HookInputValidation | hYr, returned updatedInput that failed schema validation | PreToolUse.updatedInput is checked in the pre-hook path. |
| PermissionInputValidation | n8u, PERMISSION_UPDATED_INPUT | A later permission-handler updatedInput is independently checked before tool.call. |
| PermissionDeniedRetryFeedback | The PermissionDenied hook indicated you may retry this tool call. | Model feedback from the auto-mode classifier-denial hook branch; it is not an automatic retry. |
| PreToolUseAuthorizationHook | hookPermissionResult, PreToolUse | PreToolUse participates in authorization, not just notification. |
| CanUseToolBridge | createCanUseTool | Host/SDK/Remote Control bridge wrapping the same permission resolver. |
| PermissionDeniedFrame | permission_denied | System frame for deny-shortcut decisions sent to SDK hosts. |
| CanUseToolControlRequest | sendControlRequest({subtype:"can_use_tool"...}) | Ask path surfaces as a host control request. |
| McpRuntimeCoordinator | function fH9(H) | MCP runtime coordinator; capability source for tools/resources/prompts. |
| McpCommandRegistrar | function rR4(H) | MCP command tree; user-facing config surface for the same source. |
| PluginCommandRegistrar | function fC4(H) | Plugin command tree; injects agents/skills/hooks/MCP/output styles. |
| HookEventTaxonomy | Hook arrays (PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch, PermissionRequest, PermissionDenied, …) | Hook event taxonomy used by both authorization and lifecycle. |
| SkillShellPolicySwitch | disableSkillShellExecution | Managed-policy switch for skills/custom slash commands. |
| RemoteControlPolicySwitch | disableRemoteControl | Managed-policy switch for Remote Control entry. |
| PermissionPromptToolFlag | --permission-prompt-tool | Permission prompting delegated to a schema-bearing MCP tool. |
| ReadBeforeWriteGuard | File has not been read yet. Read it first before writing to it. | Tool-specific guard inside Edit/Write/NotebookEdit. |
| McpTimeoutGuards | MCP_TIMEOUT, MCP_CONNECT_TIMEOUT_MS | Capability-source timeouts; protect the execution boundary from slow servers. |
Internal decomposition
flowchart TD Builtins[Built-in tools] --> Registry[Capability registry] McpTools[MCP servers via McpRuntimeCoordinator] --> Registry Plugins[Plugins via PluginCommandRegistrar] --> Registry Skills[Skills] --> Registry Tasks[Task / subagent tools] --> Registry External[External/SDK tool defs] --> Registry
Registry --> Filter[Visibility filter: --tools / --allowedTools / --disallowedTools / settings] Filter --> Visible[Model-visible tool schema]
Visible --> ToolUse[Tool-use delta from model] ToolUse --> Boundary[Tool execution boundary] Boundary --> Schema[Schema parse and tool validateInput] Schema --> PreHook[PreToolUse hook and hook-input check] PreHook --> Decision[Merged permission decision] Decision -->|allow with replacement| FinalInput[Independent permission-input validation] Decision -->|allow unchanged| Guards[Tool body and live guards] FinalInput --> Guards Decision -->|ordinary deny| Denial[denial result / permission_denied frame] Decision -->|auto-mode classifier deny| DenialHook[PermissionDenied hook] DenialHook --> Denial Decision -->|ask| Host[createCanUseTool -> can_use_tool control request] Host --> Decision Guards --> Execute[Tool body] Execute --> PerCall[PostToolUse or PostToolUseFailure] PerCall --> Batch[PostToolBatch after the result batch] DenialHook --> ModelFeedback[denial message and optional retry guidance] Batch --> SessionEvents[session events / telemetry]| Sub-component | Responsibility |
|---|---|
| Capability registry | Normalizes built-in, MCP, plugin, skill, task, and external tool definitions to a common shape. |
| Visibility filter | Applies --tools, --allowedTools, --disallowedTools, settings, and managed policy to decide what the model sees. |
| Permission resolver | Combines allow/deny rules, permission mode, hook output, host responses, and helper-tool prompts into one decision. |
ToolExecutionBoundary | The single place tool calls cross from “model-asked” to “actually-run.” |
| Hook dispatcher | Runs PreToolUse/PostToolUse/PermissionDenied and related events at well-defined points. |
McpRuntimeCoordinator | Connects always-load, regular, and claude.ai connector groups; bridges elicitation completion. |
PluginCommandRegistrar | Loads plugin-provided agents, skills, hooks, MCP servers, output styles, and slash commands. |
| Integration adapters | IDE auto-connect, Chrome, file-resource startup, status line, helper scripts. |
Ordering within one model response
The scheduler does not run every tool call in a model response concurrently. Y3g first checks each call’s schema shape and isConcurrencySafe predicate, then coalesces only contiguous safe calls. A schema-invalid or concurrency-unsafe call becomes a singleton block and therefore an ordering barrier. J3g runs a safe block through the bounded parallel executor, while X3g runs singleton blocks serially. K3g uses a positive CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY value when present and otherwise returns 10 (cli.renamed.js:343251-343333).
Success/failure hooks are per call, so PostToolUse or PostToolUseFailure dispatches can overlap for calls in the same parallel block. The aggregate PostToolBatch hook runs only after the result batch has settled. In the normal query loop it can append context or stop continuation before the next model request (executePostToolBatchHooks, around cli.renamed.js:462457). The end-turn path also dispatches it, but logs blocking/prevent-continuation output as discarded because the tool result or MCP metadata has already ended the turn and no model re-invocation remains (Fmy, around cli.renamed.js:459780). Abort and other termination branches should not be inferred to have the normal continuation semantics.
Public interface
Inputs
| Surface | Effect |
|---|---|
--tools, --allowedTools/--allowed-tools, --disallowedTools/--disallowed-tools, --permission-mode, --permission-prompt-tool, --dangerously-skip-permissions | Shape visibility and approval behavior. |
--mcp-config, --strict-mcp-config, claude mcp ... | Configure MCP servers and connector behavior. |
--plugin-dir, --plugin-url, claude plugin ... | Load session-only or marketplace plugins. |
--ide, --chrome, --file | Activate IDE/Chrome/file integration sources. |
.claude/settings.json, settings.local.json, managed settings | Persistent allow/deny, hook config, plugin trust, policy switches. |
Environment: MCP_TIMEOUT, MCP_CONNECT_TIMEOUT_MS, MCP_CONNECTION_NONBLOCKING | Capability-source timeouts and connection mode. |
Outputs
| Output | Consumer |
|---|---|
| Tool result (success or error) | Model loop. |
permission_denied frame | SDK hosts and Remote Control bridge. |
can_use_tool control request and permission_response | Interactive host or SDK approver. |
Hook calls (PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch, PermissionRequest, PermissionDenied) | Hook scripts/commands, telemetry. |
| MCP elicitation completion frames | HeadlessFrameMultiplexer. |
code_edit_tool.decision and tengu_tool_use_* telemetry | Telemetry sinks. |
Internal collaborators
| Collaborator | Contract |
|---|---|
| Context/model loop | Embeds tool metadata into the model-visible request; sends tool-use deltas to the boundary. |
| Sessions module | Records tool starts/results/errors as session events; resume restores tool/permission state. |
| Runtime lifecycle | Provides settings, managed policy, and CLI permission flags before the loop starts. |
| Remote/bridge module | Uses createCanUseTool to route ask/deny decisions to remote approvers and hosts. |
| Hooks subsystem | Receives lifecycle events; can mutate input, authorization, and additional context. |
| Telemetry/ops | Records denial/allow telemetry, MCP auth errors, code-edit decisions, and timeouts. |
Design decisions
- Single boundary, multiple sources. Every tool call goes through
ToolExecutionBoundaryregardless of source. This keeps telemetry, hooks, and permission policy consistent for built-in, MCP, plugin, skill, and task tools. PreToolUseparticipates in authorization. Hooks canallow,ask,deny,defer, supplyupdatedInput, or addadditionalContext. Treating hooks as authorization (not just notification) lets policy logic live outside the bundle.- Permission decisions are tri-modal. Allow/deny shortcut directly through telemetry; ask is surfaced as a structured
can_use_toolcontrol request. SDK hosts only see ask flows; deny is observable but never blocks on a host round-trip. - Auto-mode classifier denial has an extra feedback branch. When the final denial reason is specifically
classifier: "auto-mode", the boundary dispatchesPermissionDenied. A hook result withretry: trueappends the model-visible message “The PermissionDenied hook indicated you may retry this tool call.” It does not re-run the tool. Rule, mode, user, hook, and other denials still produce their normal denial result/frame, but this build does not source-confirm the samePermissionDenieddispatch for those branches. - MCP and plugins are first-class capability sources. They go through the same registry and visibility filter as built-ins; they cannot bypass the boundary.
- Required vs optional MCP.
McpRuntimeCoordinatorsplits configs intoalwaysLoadand normal groups so essential capabilities are present before the model runs, while optional servers can defer or fail without blocking startup. - Helper tools can prompt for permission.
--permission-prompt-toollets an MCP tool with a JSON schema be the approval UI; this keeps the runtime’s approval channel pluggable. - Managed policy can disable extension points.
disableSkillShellExecution,disableRemoteControl,disableAgentView, and similar switches are intentionally part of the trust pipeline; they are not separate code paths. - Tool-specific guards complement permissions. Edit/Write/NotebookEdit require a prior
Read; WebFetch enforcesdomain:syntax; WebSearch rejects wildcards. These are local invariants, not permission rules.
Trust pipeline summary
flowchart TD Source[capability source] --> Visible{visible?} Visible -->|no| Hidden[not in model schema] Visible -->|yes| Call[tool call] Call --> Schema[schema parse and validateInput] Schema --> PreHook[PreToolUse hook] PreHook --> HookCheck[validate PreToolUse updatedInput] HookCheck --> Decision[permission resolver] PreHook -->|deny| Block[deny path] PreHook -->|ask| Host[can_use_tool control_request] PreHook -->|defer| Decision Host --> Decision Decision -->|allow with updatedInput| FinalCheck[validate permission replacement] Decision -->|allow unchanged| Guards[tool body / live guards] FinalCheck --> Guards Decision -->|deny| Block Guards --> Run[tool body] Run --> Events[per-call post hook, then aggregate PostToolBatch] Block --> Frame[denial result / permission_denied frame] Block -. auto-mode classifier only .-> DeniedHook[PermissionDenied] DeniedHook --> ModelMeta[optional model retry guidance]The pipeline is intentionally one-way except for the ask loop. Hooks can shape the decision, but they cannot bypass the registry or the boundary.
Failure modes
| Failure | Behavior |
|---|---|
| Tool input fails schema validation | ToolExecutionBoundary returns a structured error before any hook or permission decision; no execution. |
| MCP tool returns 401 / token expired | tengu_mcp_tool_call_auth_error is emitted and a user-facing reauth error is raised. |
| MCP server slow or unreachable | MCP_TIMEOUT/MCP_CONNECT_TIMEOUT_MS enforce limits; coordinator can retry transient remote failures. |
| Hook script crashes or hangs | The boundary handles the missing/invalid result; lifecycle continues with a denial or a deferred decision. |
--permission-prompt-tool references a missing or non-MCP tool | Runtime writes an error and exits; this is enforced before the loop starts. |
| File edit without prior read | Read-before-write guard rejects with a precise model-facing message asking for a refresh Read. |
| Managed policy disables a capability mid-session | Visibility filter recomputes; in-flight calls complete but new calls become invisible. |
Extension points
| Extension | How it plugs in |
|---|---|
| New built-in tool | Register a constant and definition into the capability registry; rely on the existing visibility filter and boundary. |
| New MCP server | Add via --mcp-config, settings, or plugin; runtime coordinator handles connection, deduplication, and elicitation. |
| New plugin capability | Use plugin schema (agents, skills, hooks, mcpServers, outputStyles, lspServers); do not register tools directly. |
| New hook event | Extend the hook event array and ensure the relevant runtime point emits it. |
| Custom approval UI | Implement an MCP tool with a JSON schema and pass it through --permission-prompt-tool. |
| Org policy | Use managed settings switches (disableSkillShellExecution, disableRemoteControl, …) rather than patching the boundary. |
Caveats
- The capability registry’s exact internal shape is bundled; this page documents what the registry must produce, not the precise object layout.
- The ordering above is source-confirmed for the normal model loop and the observed end-turn branch in
2.1.215; do not generalize it to every abort or exceptional termination path. --dangerously-skip-permissionsis intentionally a sharp tool; documents should not describe it as a normal operating mode.
Related docs
Created and maintained by Yingting Huang.