claude-code/learn/03-tool-system.md

15 KiB

3. Tool System

How 42 built-in tools are defined, validated, orchestrated, and rendered.


Overview

Every capability Claude Code has — reading files, running bash, editing code, searching the web — is a Tool. Tools are the bridge between the model's intentions and the real world.

%%{init: {'theme': 'dark', 'themeVariables': { 'primaryColor': '#1a1a2e', 'primaryTextColor': '#e0e0e0', 'primaryBorderColor': '#28a745', 'lineColor': '#28a745', 'secondaryColor': '#16213e', 'tertiaryColor': '#0f3460'}}}%%
graph TB
    subgraph Interface["Tool Interface — Tool.ts"]
        direction LR
        IS["inputSchema<br/>Zod validation"]
        CP["checkPermissions"]
        CALL["call — execute"]
        PROMPT["prompt — model instructions"]
        RENDER["render — terminal UI"]
    end

    IS --> CP --> CALL --> PROMPT --> RENDER

    subgraph FileOps["File Operations"]
        FR["FileRead"]
        FW["FileWrite"]
        FE["FileEdit"]
        GL["Glob"]
        GR["Grep"]
        NE["NotebookEdit"]
    end

    subgraph Exec["Execution"]
        BA["Bash"]
        PS["PowerShell"]
    end

    subgraph Web["Web"]
        WF["WebFetch"]
        WS["WebSearch"]
    end

    subgraph AgentTools["Agent and Task"]
        AG["Agent — spawn sub-agent"]
        TC["TaskCreate"]
        TG["TaskGet"]
        TU["TaskUpdate"]
        TL["TaskList"]
        TS["TaskStop"]
        SM["SendMessage"]
    end

    subgraph Meta["Meta Tools"]
        AQ["AskUserQuestion"]
        SK["SkillTool"]
        TW["TodoWrite"]
        EP["EnterPlanMode"]
        XP["ExitPlanMode"]
        TSR["ToolSearch"]
    end

    subgraph Dynamic["Dynamic — Runtime Loaded"]
        MCP_T["MCP Tools<br/>from external servers"]
        LSP_T["LSP Tool<br/>language server queries"]
    end

    subgraph Orchestration["Orchestration Layer"]
        RUN["toolOrchestration.ts<br/>runTools — parallel dispatch"]
        STE["StreamingToolExecutor<br/>execute as blocks stream in"]
        TEX["toolExecution.ts — 60KB<br/>single tool lifecycle"]
        THK["toolHooks.ts<br/>Pre/Post hook dispatch"]
    end

    Interface --> FileOps
    Interface --> Exec
    Interface --> Web
    Interface --> AgentTools
    Interface --> Meta
    Interface --> Dynamic

    FileOps --> Orchestration
    Exec --> Orchestration
    Web --> Orchestration
    AgentTools --> Orchestration
    Meta --> Orchestration
    Dynamic --> Orchestration

    RUN --> STE
    RUN --> TEX
    TEX --> THK

The Tool Interface — Tool.ts (793 lines)

Every tool implements the Tool<Input, Output, Progress> type. Here are the key methods:

%%{init: {'theme': 'dark', 'themeVariables': { 'primaryColor': '#1a1a2e', 'primaryTextColor': '#e0e0e0', 'lineColor': '#28a745', 'primaryBorderColor': '#28a745'}}}%%
flowchart LR
    subgraph Definition["Tool Definition"]
        NAME["name: string"]
        SCHEMA["inputSchema: Zod"]
        ALIASES["aliases?: string array"]
        HINT["searchHint?: string"]
    end

    subgraph Lifecycle["Lifecycle Methods"]
        VAL["validateInput<br/>pre-execution check"]
        PERM["checkPermissions<br/>allow / deny / prompt"]
        CALL["call<br/>execute the tool"]
        DESC["description<br/>model-facing summary"]
    end

    subgraph Rendering["Rendering Methods"]
        RUM["renderToolUseMessage<br/>show input in terminal"]
        RRM["renderToolResultMessage<br/>show output in terminal"]
        RPM["renderToolUseProgressMessage<br/>spinner / progress bar"]
        GRP["renderGroupedToolUse<br/>parallel display"]
    end

    subgraph Metadata["Metadata Methods"]
        RO["isReadOnly<br/>does it write?"]
        CS["isConcurrencySafe<br/>parallel safe?"]
        EN["isEnabled<br/>available now?"]
        DS["isDestructive<br/>irreversible?"]
        AC["toAutoClassifierInput<br/>safety classifier text"]
    end

    Definition --> Lifecycle --> Rendering
    Definition --> Metadata

The buildTool Factory

All tools go through buildTool() which provides safe defaults:

const TOOL_DEFAULTS = {
  isEnabled: () => true,
  isConcurrencySafe: () => false,    // Assume not safe
  isReadOnly: () => false,            // Assume writes
  isDestructive: () => false,
  checkPermissions: (input) =>        // Defer to general system
    Promise.resolve({ behavior: 'allow', updatedInput: input }),
  toAutoClassifierInput: () => '',    // Skip classifier
  userFacingName: () => '',
}

export function buildTool(def) {
  return { ...TOOL_DEFAULTS, userFacingName: () => def.name, ...def }
}

This "fail-closed" design means a tool that forgets to implement isConcurrencySafe defaults to false (not safe for parallel execution).


The 42 Built-in Tools

%%{init: {'theme': 'dark', 'themeVariables': { 'primaryColor': '#1a1a2e', 'primaryTextColor': '#e0e0e0', 'lineColor': '#17a2b8', 'primaryBorderColor': '#17a2b8'}}}%%
graph TB
    subgraph FileOps["File Operations — Read + Write + Search"]
        FR["FileRead<br/>Read files, images,<br/>PDFs, notebooks"]
        FW["FileWrite<br/>Create or overwrite<br/>entire files"]
        FE["FileEdit<br/>Partial string<br/>replacement edits"]
        GL["Glob<br/>File pattern<br/>matching search"]
        GR["Grep<br/>ripgrep content<br/>search"]
        NE["NotebookEdit<br/>Jupyter notebook<br/>cell editing"]
    end

    subgraph Exec["Execution — Run Commands"]
        BA["Bash<br/>Shell command<br/>execution"]
        PS["PowerShell<br/>Windows shell<br/>execution"]
        REPL["REPL<br/>Persistent JS/TS<br/>runtime context"]
    end

    subgraph Web["Web — Fetch and Search"]
        WF["WebFetch<br/>HTTP GET to URLs<br/>HTML to markdown"]
        WS["WebSearch<br/>Web search via<br/>Brave or similar"]
    end

    subgraph AgentTask["Agent and Task Management"]
        AG["Agent<br/>Spawn sub-agent<br/>with forked context"]
        TC["TaskCreate<br/>Background task"]
        TG["TaskGet<br/>Check task status"]
        TU["TaskUpdate<br/>Update task state"]
        TL["TaskList<br/>List all tasks"]
        TS["TaskStop<br/>Terminate task"]
        SM["SendMessage<br/>Inter-agent<br/>messaging"]
        TmC["TeamCreate<br/>Create agent team"]
        TmD["TeamDelete<br/>Remove agent team"]
    end

    subgraph Meta["Meta Tools — Control Claude's Behavior"]
        AQ["AskUserQuestion<br/>Interactive prompt"]
        SK["SkillTool<br/>Execute skills"]
        TW["TodoWrite<br/>Manage task lists"]
        EP["EnterPlanMode<br/>Switch to read-only"]
        XP["ExitPlanMode<br/>Resume full access"]
        TSR["ToolSearch<br/>Find deferred tools"]
        BF["Brief<br/>Toggle concise mode"]
        SL["Sleep<br/>Idle wait for<br/>proactive mode"]
        SO["SyntheticOutput<br/>Structured JSON<br/>output"]
    end

    subgraph Dynamic["Dynamic — Loaded at Runtime"]
        MCP["MCP Tools<br/>From external<br/>MCP servers"]
        LSP["LSP Tool<br/>Language server<br/>queries"]
    end

    subgraph Special["Special Purpose"]
        EW["EnterWorktree<br/>Git worktree<br/>isolation"]
        XW["ExitWorktree<br/>Leave worktree"]
        RT["RemoteTrigger<br/>Remote execution"]
        SC["ScheduleCron<br/>Timed triggers"]
        CF["Config<br/>Settings management"]
    end

Tool Orchestration — Parallel Execution

When the model returns multiple tool_use blocks, Claude Code can execute them in parallel:

%%{init: {'theme': 'dark', 'themeVariables': { 'primaryColor': '#1a1a2e', 'primaryTextColor': '#e0e0e0', 'lineColor': '#4a9eff', 'primaryBorderColor': '#4a9eff'}}}%%
sequenceDiagram
    participant Q as query.ts
    participant O as toolOrchestration.ts
    participant STE as StreamingToolExecutor
    participant T1 as Tool 1 — FileRead
    participant T2 as Tool 2 — Grep
    participant T3 as Tool 3 — Bash
    participant P as Permission System

    Q->>O: runTools(3 tool_use blocks)
    activate O

    Note over O: Check concurrency safety

    O->>STE: FileRead — isConcurrencySafe = true
    O->>STE: Grep — isConcurrencySafe = true
    O->>STE: Bash — isConcurrencySafe = false

    par Parallel Execution
        STE->>P: checkPermissions(FileRead)
        P-->>STE: allow
        STE->>T1: call(input)
        T1-->>STE: result

        STE->>P: checkPermissions(Grep)
        P-->>STE: allow
        STE->>T2: call(input)
        T2-->>STE: result
    end

    Note over STE: Wait for parallel tools

    STE->>P: checkPermissions(Bash)
    P-->>STE: prompt user
    STE->>T3: call(input)
    T3-->>STE: result

    O-->>Q: yield all tool_result messages
    deactivate O

Key files in the orchestration layer:

  • toolOrchestration.tsrunTools(): dispatches tools, handles parallel vs. sequential
  • StreamingToolExecutor — Starts permission checks while model is still streaming
  • toolExecution.ts (60KB) — Single tool lifecycle: validate → permissions → execute → hooks
  • toolHooks.ts — Dispatches PreToolUse and PostToolUse hooks

Single Tool Lifecycle

%%{init: {'theme': 'dark', 'themeVariables': { 'primaryColor': '#1a1a2e', 'primaryTextColor': '#e0e0e0', 'lineColor': '#28a745', 'primaryBorderColor': '#28a745'}}}%%
flowchart TD
    BLOCK["tool_use block arrives<br/>from model stream"]:::start

    PARSE["Parse + validate input<br/>via Zod inputSchema"]:::step
    VAL{"validateInput?"}:::check

    DENY_VAL["Return error to model<br/>with validation message"]:::deny

    PRE_HOOK["Run PreToolUse hooks<br/>user-defined scripts"]:::hook
    HOOK_R{"Hook result?"}:::check

    PERM["Check permissions<br/>deny → allow → tool → hooks → classifier → dialog"]:::step
    PERM_R{"Permission?"}:::check

    EXEC["tool.call(input, context)<br/>execute the operation"]:::step
    RESULT["Map output to tool_result<br/>via mapToolResultToToolResultBlockParam"]:::step

    SIZE{"Result exceeds<br/>maxResultSizeChars?"}:::check
    PERSIST["Persist to disk<br/>return file path + preview"]:::step

    POST_HOOK["Run PostToolUse hooks"]:::hook
    RENDER["Render in terminal<br/>renderToolResultMessage"]:::step

    YIELD["Yield tool_result<br/>to query loop"]:::done

    DENY_PERM["Return permission_denied<br/>error to model"]:::deny

    BLOCK --> PARSE --> VAL
    VAL -->|"pass"| PRE_HOOK
    VAL -->|"fail"| DENY_VAL

    PRE_HOOK --> HOOK_R
    HOOK_R -->|"approve"| PERM
    HOOK_R -->|"deny"| DENY_PERM
    HOOK_R -->|"modify input"| PERM

    PERM --> PERM_R
    PERM_R -->|"allow"| EXEC
    PERM_R -->|"deny"| DENY_PERM

    EXEC --> RESULT --> SIZE
    SIZE -->|"within limit"| POST_HOOK
    SIZE -->|"exceeds limit"| PERSIST --> POST_HOOK

    POST_HOOK --> RENDER --> YIELD

    classDef start fill:#1a2d4a,stroke:#4a9eff,color:#e0e0e0,stroke-width:2px
    classDef step fill:#1b3a1b,stroke:#28a745,color:#e0e0e0,stroke-width:2px
    classDef check fill:#2d2d0d,stroke:#ffc107,color:#e0e0e0,stroke-width:2px
    classDef hook fill:#3d2b00,stroke:#fd7e14,color:#e0e0e0,stroke-width:2px
    classDef deny fill:#4a1a1a,stroke:#dc3545,color:#e0e0e0,stroke-width:2px
    classDef done fill:#0d4f4f,stroke:#17a2b8,color:#e0e0e0,stroke-width:2px

ToolSearch — Deferred Tool Loading

With 42+ tools, sending all schemas to the model wastes tokens. ToolSearch defers tools that aren't immediately needed:

%%{init: {'theme': 'dark', 'themeVariables': { 'primaryColor': '#1a1a2e', 'primaryTextColor': '#e0e0e0', 'lineColor': '#ffc107', 'primaryBorderColor': '#ffc107'}}}%%
flowchart LR
    ALL["42+ Tools"]:::input

    SPLIT{"shouldDefer?"}:::check

    EAGER["~15 Eager Tools<br/>Always in prompt<br/>FileRead, Bash, Grep..."]:::eager
    DEFER["~27 Deferred Tools<br/>Schema not sent initially<br/>TaskCreate, WebSearch..."]:::defer
    ALWAYS["alwaysLoad Tools<br/>Forced eager by MCP meta"]:::eager

    SEARCH["ToolSearch Tool<br/>Model searches by keyword<br/>using searchHint"]:::tool

    FOUND["Tool schema injected<br/>into next request"]:::result

    ALL --> SPLIT
    SPLIT -->|"no"| EAGER
    SPLIT -->|"yes"| DEFER
    SPLIT -->|"alwaysLoad"| ALWAYS

    DEFER --> SEARCH
    SEARCH --> FOUND

    classDef input fill:#1a2d4a,stroke:#4a9eff,color:#e0e0e0,stroke-width:2px
    classDef check fill:#2d2d0d,stroke:#ffc107,color:#e0e0e0,stroke-width:2px
    classDef eager fill:#1b3a1b,stroke:#28a745,color:#e0e0e0,stroke-width:2px
    classDef defer fill:#3d2b00,stroke:#fd7e14,color:#e0e0e0,stroke-width:2px
    classDef tool fill:#2d1b4e,stroke:#6f42c1,color:#e0e0e0,stroke-width:2px
    classDef result fill:#0d4f4f,stroke:#17a2b8,color:#e0e0e0,stroke-width:2px

Dynamic Tools — MCP and LSP

Beyond built-in tools, Claude Code loads tools dynamically at runtime:

%%{init: {'theme': 'dark', 'themeVariables': { 'primaryColor': '#1a1a2e', 'primaryTextColor': '#e0e0e0', 'lineColor': '#17a2b8', 'primaryBorderColor': '#17a2b8'}}}%%
flowchart TD
    subgraph MCP["MCP Tools — Model Context Protocol"]
        SRV["External MCP Servers<br/>configured in settings"]
        CONN["MCPConnectionManager<br/>stdio / SSE transport"]
        DISC["Discover tools<br/>via tools/list"]
        WRAP["Wrap as Tool objects<br/>name: mcp__server__tool"]
    end

    subgraph LSP["LSP Tool — Language Server Protocol"]
        LS["Language Server<br/>runtime type info"]
        QUERY_LSP["Query definitions,<br/>references, diagnostics"]
    end

    SRV --> CONN --> DISC --> WRAP
    LS --> QUERY_LSP

    MERGE["Merged into tool pool<br/>via useMergedTools hook"]:::merge

    WRAP --> MERGE
    QUERY_LSP --> MERGE

    classDef merge fill:#1b3a1b,stroke:#28a745,color:#e0e0e0,stroke-width:2px

MCP tools are prefixed with mcp__<server>__<tool> unless running in SDK no-prefix mode. They go through the same permission system as built-in tools.


Key Design Decisions

1. Self-Contained Modules

Each tool directory (src/tools/<ToolName>/) contains everything:

  • index.ts — Tool definition via buildTool()
  • prompt.ts — Model-facing instructions
  • *.test.ts — Tests
  • Additional helpers as needed

2. Fail-Closed Defaults

buildTool() defaults are conservative:

  • isConcurrencySafe = false — Won't run in parallel unless explicitly safe
  • isReadOnly = false — Assumed to write unless stated otherwise
  • checkPermissions defaults to allow — But the general permission system still applies

3. Result Size Budgets

Each tool has maxResultSizeChars. Oversized results are persisted to disk and the model gets a truncated preview + file path. This prevents single tool results from consuming the entire context window.

4. Observable Input Backfilling

backfillObservableInput() adds derived fields to tool inputs for SDK consumers and transcripts, without mutating the API-bound input (which would break prompt caching):

// The API sees: { file_path: "src/foo.ts" }
// SDK/transcript sees: { file_path: "src/foo.ts", resolved_path: "/abs/path/src/foo.ts" }

Previous: ← The Agentic Loop · Next: Permission System →