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

446 lines
15 KiB
Markdown

# 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.
```mermaid
%%{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:
```mermaid
%%{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:
```typescript
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
```mermaid
%%{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**:
```mermaid
%%{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.ts`** — `runTools()`: 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
```mermaid
%%{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:
```mermaid
%%{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:
```mermaid
%%{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):
```typescript
// 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](./02-agentic-loop.md) · **Next:** [Permission System →](./04-permission-system.md)