Chapter 2: Tool System — 40+ Tools as the Model's Hands
Why the Tool System Is the Core of Claude Code
Large language models "think" in the text domain, while software engineering operations happen in the file system, terminal, and network. The tool system is the bridge connecting these two worlds: it translates the model's intent into real side effects, then translates the side effects' results back into text the model can consume.
Claude Code's tool system manages 40+ built-in tools and an unlimited number of MCP extension tools. These tools are not a flat array — they pass through a precise pipeline: Definition -> Registration -> Filtering -> Invocation -> Rendering. Each step has a clear contract. This chapter starts from the Tool.ts interface definition and dissects each layer of this pipeline's design decisions.
2.1 The Tool Interface Contract
All tools — whether the built-in BashTool or third-party tools loaded via the MCP protocol — must satisfy the same TypeScript interface. This interface is defined in restored-src/src/Tool.ts:362-695 and is the cornerstone of the entire tool system.
Core Fields Overview
| Field | Type | Responsibility | Required |
|---|---|---|---|
name | readonly string | Unique identifier for the tool, used for permission matching, analytics, and API transport | Yes |
description | (input, options) => Promise<string> | Returns the tool description text sent to the model; can dynamically adjust based on permission context | Yes |
prompt | (options) => Promise<string> | Returns the tool's system prompt, see Chapter 8 | Yes |
inputSchema | z.ZodType (Zod v4) | Defines the tool's parameter structure using Zod schema, automatically converted to JSON Schema for the API | Yes |
call | (args, context, canUseTool, parentMessage, onProgress?) => Promise<ToolResult> | The tool's core execution logic | Yes |
checkPermissions | (input, context) => Promise<PermissionResult> | Tool-level permission check, executed after the general permission system | Yes* |
validateInput | (input, context) => Promise<ValidationResult> | Validates input legality before permission checking | No |
maxResultSizeChars | number | Character limit for a single tool result; exceeding this persists to disk | Yes |
isConcurrencySafe | (input) => boolean | Whether it can execute concurrently with other tools | Yes* |
isReadOnly | (input) => boolean | Whether it's a read-only operation (doesn't modify the file system) | Yes* |
isEnabled | () => boolean | Whether the tool is available in the current environment | Yes* |
Fields marked * have default values provided by
buildTool()and can be omitted in tool definitions.
Several design choices worth examining in depth:
description is a function, not a string. The same tool may need different descriptions under different permission modes. For example, when a user configures an alwaysDeny rule prohibiting certain subcommands, the tool description can proactively inform the model "don't attempt these operations," avoiding useless tool calls at the prompt level.
inputSchema uses Zod v4. This allows strict runtime validation of tool parameters while automatically generating JSON Schema for the Anthropic API via z.toJSONSchema(). Zod's z.strictObject() ensures the model doesn't pass undefined parameters.
call receives a canUseTool callback. This is an extremely important design — a tool may need to recursively check permissions for sub-operations during execution. For example, AgentTool needs to check whether a sub-Agent has permission to use specific tools when spawning it. Permission checking is not a one-time gate but a continuous verification throughout the execution process.
Rendering Contract: Three Method Groups
The Tool interface defines a set of rendering methods that constitute the tool's complete lifecycle representation in the terminal UI (see Section 2.5):
renderToolUseMessage // Displayed when the tool is invoked
renderToolUseProgressMessage // Displays progress during execution
renderToolResultMessage // Displays results after execution completes
There are also optional methods like renderToolUseErrorMessage, renderToolUseRejectedMessage (permission denied), and renderGroupedToolUse (grouped display of parallel tools).
2.2 The buildTool() Factory Function and Fail-Closed Defaults
Every concrete tool is not directly exported as an object satisfying the Tool interface but is constructed through the buildTool() factory function. This function is defined in restored-src/src/Tool.ts:783-792:
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
return {
...TOOL_DEFAULTS,
userFacingName: () => def.name,
...def,
} as BuiltTool<D>
}
The runtime behavior is minimal — just an object spread. But its type-level design (BuiltTool<D> type) precisely models the semantics of { ...TOOL_DEFAULTS, ...def }: if the tool definition provides a method, use the tool definition's version; otherwise use the default.
Defaults and the "Fail-Closed" Philosophy
TOOL_DEFAULTS (restored-src/src/Tool.ts:757-769) is designed following a safety principle — assume the most dangerous scenario when uncertain:
| Default Method | Default Value | Design Intent |
|---|---|---|
isEnabled | () => true | Tools are available by default unless explicitly disabled |
isConcurrencySafe | () => false | Fail-closed: Assume unsafe, prohibit concurrency |
isReadOnly | () => false | Fail-closed: Assume it writes, require permissions |
isDestructive | () => false | Non-destructive by default |
checkPermissions | Returns { behavior: 'allow' } | Delegate to the general permission system |
toAutoClassifierInput | () => '' | Doesn't participate in automatic safety classification by default |
userFacingName | () => def.name | Uses the tool name |
The two most important defaults are isConcurrencySafe: false and isReadOnly: false. This means: if a new tool forgets to declare these properties, the system automatically treats it as "may modify the file system and cannot execute concurrently" — the most conservative, safest assumption. Only when a tool developer actively declares isConcurrencySafe() { return true } and isReadOnly() { return true } does the system relax restrictions.
How Actual Tools Use buildTool
Taking GrepTool as an example (restored-src/src/tools/GrepTool/GrepTool.ts:160-194):
export const GrepTool = buildTool({
name: GREP_TOOL_NAME,
searchHint: 'search file contents with regex (ripgrep)',
maxResultSizeChars: 20_000,
strict: true,
// ...
isConcurrencySafe() { return true }, // Search is a safe concurrent operation
isReadOnly() { return true }, // Search doesn't modify files
// ...
})
GrepTool explicitly overrides two defaults because search operations are inherently read-only and concurrency-safe. In contrast, BashTool (restored-src/src/tools/BashTool/BashTool.tsx:434-441) has conditional concurrency safety:
isConcurrencySafe(input) {
return this.isReadOnly?.(input) ?? false;
},
isReadOnly(input) {
const compoundCommandHasCd = commandHasAnyCd(input.command);
const result = checkReadOnlyConstraints(input, compoundCommandHasCd);
return result.behavior === 'allow';
},
BashTool only allows concurrency when the command is determined to be read-only — a git status can execute concurrently, but git push cannot. This input-aware concurrency control is why buildTool's method signatures accept an input parameter.
2.3 Tool Registration Pipeline: tools.ts
restored-src/src/tools.ts is the assembly center for the Tool Pool. It answers a core question: In the current environment, which tools can the model use?
Three-Level Filtering
Tools go through three levels of filtering from definition to final availability:
Level 1: Compile-time/startup-time conditional loading. Many tools are conditionally loaded via Feature Flags (restored-src/src/tools.ts:16-135):
const SleepTool =
feature('PROACTIVE') || feature('KAIROS')
? require('./tools/SleepTool/SleepTool.js').SleepTool
: null
const cronTools = feature('AGENT_TRIGGERS')
? [
require('./tools/ScheduleCronTool/CronCreateTool.js').CronCreateTool,
require('./tools/ScheduleCronTool/CronDeleteTool.js').CronDeleteTool,
require('./tools/ScheduleCronTool/CronListTool.js').CronListTool,
]
: []
The feature() function comes from bun:bundle and is evaluated at bundle time. This means disabled tools don't appear in the final JavaScript bundle at all — a more thorough form of dead code elimination than runtime if statements.
Besides Feature Flags, there's also environment variable-driven conditional loading:
const REPLTool =
process.env.USER_TYPE === 'ant'
? require('./tools/REPLTool/REPLTool.js').REPLTool
: null
USER_TYPE === 'ant' marks special tools for Anthropic internal employees (like REPLTool, ConfigTool, TungstenTool), which are unavailable in the public version.
Level 2: getAllBaseTools() assembles the base tool pool. This function (restored-src/src/tools.ts:193-251) collects all tools that passed Level 1 filtering into an array. It's the system's "tool registry" — all potentially existing tools are registered here. The current version contains about 40+ built-in tools, dynamically adjusted based on which Feature Flags are enabled.
export function getAllBaseTools(): Tools {
return [
AgentTool,
TaskOutputTool,
BashTool,
...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
FileReadTool,
FileEditTool,
FileWriteTool,
// ... 30+ more tools omitted
...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
]
}
Note an interesting condition: hasEmbeddedSearchTools(). In Anthropic's internal builds, bfs (fast find) and ugrep are embedded in the Bun binary, at which point find and grep in the shell are already aliased to these fast tools, making standalone GlobTool and GrepTool redundant.
Level 3: getTools() runtime filtering. This is the final filtering layer (restored-src/src/tools.ts:271-327), performing three operations:
- Permission denial filtering:
filterToolsByDenyRules()removes tools covered byalwaysDenyrules. If a user configures"Bash": "deny",BashToolwon't appear in the tool list sent to the model at all. - REPL mode hiding: When REPL mode is enabled,
Bash,Read,Edit, and other basic tools are hidden — they're indirectly exposed throughREPLTool's VM context. isEnabled()final check: Each tool'sisEnabled()method is the last switch.
Simple Mode vs. Full Mode
getTools() also supports a "simple mode" (CLAUDE_CODE_SIMPLE), exposing only Bash, FileRead, and FileEdit — three core tools. This is useful in some integration scenarios — reducing the number of tools lowers token consumption and reduces the model's decision burden.
MCP Tool Integration
The final tool pool is assembled by assembleToolPool() (restored-src/src/tools.ts:345-367):
export function assembleToolPool(
permissionContext: ToolPermissionContext,
mcpTools: Tools,
): Tools {
const builtInTools = getTools(permissionContext)
const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
return uniqBy(
[...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
'name',
)
}
Two key designs here:
- Built-in tools take priority:
uniqByretains the first occurrence of each name; built-in tools are listed first, so they win in name conflicts. - Sorting by name for stable prompt caching: Built-in tools and MCP tools are each sorted and then concatenated (rather than interleaved), ensuring built-in tools appear as a "contiguous prefix." This works with the API server-side's cache breakpoint design — if MCP tools were interspersed among built-in tools, any addition or removal of an MCP tool would invalidate all downstream cache keys. See Chapter 13.
2.4 Tool Result Size Budget
When a tool returns a result, the system faces a core tension: the model needs to see complete information to make correct decisions, but the context window is limited. Claude Code solves this through a two-level budget.
Level 1: Per-Tool Result Limit maxResultSizeChars
Each tool declares its own result size limit via the maxResultSizeChars field. Results exceeding this limit are persisted to disk, and the model only sees a preview plus the disk file path.
Here's a comparison of maxResultSizeChars across different tools:
| Tool | maxResultSizeChars | Notes |
|---|---|---|
McpAuthTool | 10,000 | Auth results, small data volume |
GrepTool | 20,000 | Search results need to be concise |
BashTool | 30,000 | Shell output can be lengthy |
GlobTool | 100,000 | File lists can be numerous |
AgentTool | 100,000 | Sub-Agent results |
WebSearchTool | 100,000 | Web search results |
BriefTool | 100,000 | Brief summaries |
FileReadTool | Infinity | Never persisted (see below) |
FileReadTool's maxResultSizeChars: Infinity is a special design — avoiding the circular reference of Read -> persist to file -> Read. The system also has a global ceiling DEFAULT_MAX_RESULT_SIZE_CHARS = 50,000 (restored-src/src/constants/toolLimits.ts:13), which serves as a hard cap regardless of what a tool declares.
Level 2: Per-Message Aggregate Limit
When the model calls multiple tools in parallel within a single turn, all tool results are sent as multiple tool_result blocks within the same user message. MAX_TOOL_RESULTS_PER_MESSAGE_CHARS = 200,000 (restored-src/src/constants/toolLimits.ts:49) limits the total size of tool results in a single message, preventing N parallel tools from collectively overwhelming the context window.
The FileReadTool Infinity design rationale, per-message budget persistence implementation details (including ContentReplacementState decision persistence and the Infinity exemption mechanism) are covered in Chapter 4.
Size Budget Parameters Summary
| Constant | Value | Definition Location |
|---|---|---|
DEFAULT_MAX_RESULT_SIZE_CHARS | 50,000 chars | constants/toolLimits.ts:13 |
MAX_TOOL_RESULT_TOKENS | 100,000 tokens | constants/toolLimits.ts:22 |
MAX_TOOL_RESULT_BYTES | 400,000 bytes | constants/toolLimits.ts:33 (= 100K tokens x 4 bytes/token) |
MAX_TOOL_RESULTS_PER_MESSAGE_CHARS | 200,000 chars | constants/toolLimits.ts:49 |
TOOL_SUMMARY_MAX_LENGTH | 50 chars | constants/toolLimits.ts:57 |
2.5 Three-Phase Rendering Flow
A tool's presentation in the terminal UI is not a one-time event but a three-phase progressive process. These three phases correspond one-to-one with the tool execution lifecycle.
Flow Diagram
flowchart TD
A["Model emits tool_use block<br/>(parameters may not be fully streamed yet)"] --> B
B["Phase 1: renderToolUseMessage<br/>Tool invoked, display name and parameters<br/>Parameters are Partial<Input> (streaming)"]
B -->|Tool starts executing| C
C["Phase 2: renderToolUseProgressMessage<br/>Executing, show progress<br/>Updated via onProgress callback"]
C -->|Tool execution complete| D
D["Phase 3: renderToolResultMessage<br/>Complete, display results"]
Phase 1: renderToolUseMessage — Intent Display
When the model outputs a tool_use block, this method is called immediately. Note the key type in its signature:
renderToolUseMessage(
input: Partial<z.infer<Input>>, // Note: Partial!
options: { theme: ThemeName; verbose: boolean; commands?: Command[] },
): React.ReactNode
input is Partial because: the API returns tool parameter JSON in a streaming fashion, and before JSON parsing is complete, only some fields are available. The UI must be able to render even when parameters are incomplete — users shouldn't see a blank screen.
Taking BashTool as an example, even if the command field hasn't been fully received, the UI can already display the "Bash" label and the partial command text received so far.
Phase 2: renderToolUseProgressMessage — Process Visibility
This is an optional method. For long-running tools (like BashTool, AgentTool), progress feedback is crucial. BashTool starts showing progress after a shell command has been executing for more than 2 seconds (PROGRESS_THRESHOLD_MS = 2000, restored-src/src/tools/BashTool/BashTool.tsx:55).
Progress is delivered via the onProgress callback. Each tool's progress data structure is different — BashTool's BashProgress contains stdout/stderr fragments, while AgentTool's AgentToolProgress contains the sub-Agent's message stream. These types are uniformly defined in restored-src/src/types/tools.ts, constrained through the ToolProgressData union type.
Phase 3: renderToolResultMessage — Result Presentation
This is also an optional method — when omitted, tool results are not rendered in the terminal (for example, TodoWriteTool's results are shown through a dedicated panel, not in the conversation flow).
renderToolResultMessage accepts a style?: 'condensed' option. In non-verbose mode, search-type tools (GrepTool, GlobTool) display concise summaries (like "Found 42 files across 3 directories"), while in verbose mode they show full results. Tools can use isResultTruncated(output) to tell the UI whether the current result is truncated, enabling "click to expand" interaction in fullscreen mode.
Grouped Rendering: renderGroupedToolUse
When the model calls multiple tools of the same type in parallel within a single turn (e.g., 5 Grep searches), rendering each individually would consume significant screen space. The renderGroupedToolUse method allows tools to merge multiple parallel calls into a compact grouped view — for example, "Searched 5 patterns, found 127 results across 34 files."
This method only takes effect in non-verbose mode. In verbose mode, each tool call is still rendered independently at its original position, ensuring no information is lost during debugging.
2.6 Design Patterns from Specific Tools
BashTool: The Most Complex Tool
BashTool (restored-src/src/tools/BashTool/BashTool.tsx) is the most complex single tool in the entire tool system, because the semantic space of shell commands is infinite. It needs to:
- Parse command structure to determine if it's read-only (via
checkReadOnlyConstraintsandparseForSecurity) - Understand pipes and compound commands (
ls && echo "---" && lsis still read-only) - Conditional concurrency: Only read-only commands can execute concurrently
- Progress tracking: Commands running longer than 2 seconds display streaming stdout output
- File change tracking: Record file modifications caused by shell commands via
fileHistoryTrackEditandtrackGitOperations - Sandbox execution: Execute in isolation via
SandboxManagerunder certain conditions
BashTool's maxResultSizeChars is set to 30,000 — more generous than GrepTool's 20,000, because shell output typically contains more structured information (compilation errors, test results, etc.) and the model needs to see enough context to make correct decisions.
GrepTool: The Exemplar of Concurrency Safety
GrepTool's design is relatively clean. It unconditionally declares isConcurrencySafe: true and isReadOnly: true, because search operations never modify the file system. Its maxResultSizeChars is set to 20,000 — search results exceeding this length suggest the model's search scope is too broad, and persisting to disk with a preview actually helps the model adjust its strategy.
FileReadTool: The Philosophy of Infinity
FileReadTool sets maxResultSizeChars to Infinity, choosing instead to control output size through its own maxTokens and maxSizeBytes limits. This avoids the circular read problem mentioned earlier and means FileReadTool's results are never replaced with disk references — the model always sees file content directly.
2.7 Deferred Loading and ToolSearch
When the number of tools exceeds a certain threshold (especially after many MCP tools are connected), sending all tools' complete schemas to the model consumes substantial tokens. Claude Code solves this through a Deferred Loading mechanism.
Tools marked with shouldDefer: true only send the tool name in the initial prompt (defer_loading: true), not the full parameter schema. The model must first call ToolSearchTool to search by keyword and retrieve the tool's full definition before it can call these deferred tools.
Each tool's searchHint field is designed for this purpose — it provides a 3-10 word capability description to help ToolSearchTool perform keyword matching. For example, GrepTool's searchHint is 'search file contents with regex (ripgrep)'.
Tools marked with alwaysLoad: true are never deferred — their full schema always appears in the initial prompt. This is for core tools that the model must be able to call directly in the first conversation turn.
2.8 Pattern Extraction
From Claude Code's tool system design, several patterns of universal value for AI Agent builders can be extracted:
Pattern 1: Fail-closed defaults. buildTool()'s defaults assume the most dangerous scenario (not concurrency-safe, not read-only), requiring tool developers to actively declare safe properties. This flips safety from "opt-in" to "opt-out," significantly reducing risk from omissions.
Pattern 2: Layered budget control. Single tool results have a cap, and single messages also have an aggregate cap. The two levels complement each other — per-tool limits prevent single-point runaway, and message limits prevent collective explosion from parallel calls.
Pattern 3: Input-aware properties. isConcurrencySafe(input) and isReadOnly(input) receive the tool input rather than making global judgments. The same BashTool has completely different safety properties for ls versus rm. This fine-grained input awareness is the foundation for precise permission control. See Chapter 4.
Pattern 4: Progressive rendering. Three-phase rendering (intent -> progress -> result) gives users visibility at every stage of tool execution. The Partial<Input> design ensures the UI isn't blank even during parameter streaming. This is critical for user trust — users need to know what the Agent is doing, rather than staring at a spinning loading icon.
Pattern 5: Compile-time elimination vs. runtime filtering. Feature Flags use bun:bundle's feature() to eliminate disabled tool code at compile time, while permission rules filter the tool list at runtime. The two mechanisms serve different purposes: the former reduces bundle size and attack surface, the latter supports user-level configuration.
What You Can Do
Based on Claude Code's tool system design experience, here are actions you can take when building your own AI Agent tool system:
- Adopt "fail-closed" defaults. In your tool registration framework, set the default values of safety properties like
isConcurrencySafeandisReadOnlyto the most conservative options. Have tool developers actively declare safety properties rather than assuming safety by default. - Set result size limits for every tool. Don't let tools return infinitely large results. Set per-tool limits (like
maxResultSizeChars) and per-message aggregate limits; when exceeded, persist to disk and return a preview. - Make tool descriptions functions, not static strings. If your tools have different behavior restrictions under different permission modes or contexts, dynamically generating descriptions can guide the model to avoid invalid calls at the prompt level.
- Implement three-phase rendering. Provide progress feedback for long-running tools (intent display -> execution progress -> final result), so users always know what the Agent is doing. Support
Partial<Input>to enable rendering even during parameter streaming. - Use conditional loading to reduce the tool set. Filter out unneeded tools at compile time/startup through Feature Flags or environment variables, reducing token consumption and model decision burden. For scenarios with many MCP tools, consider a deferred loading mechanism.
- Keep tool ordering stable. If you use API prompt caching, ensure the tool list order remains stable across requests. Place built-in tools as a contiguous prefix, append MCP tools sorted by name, and avoid frequent cache key invalidation.
Summary
Claude Code's tool system is a carefully layered architecture: the Tool interface defines the contract, buildTool() provides safe defaults, the tools.ts registration pipeline assembles the tool pool through compile-time and runtime two-level filtering, size budget mechanisms control context consumption at both per-tool and per-message levels, and three-phase rendering makes the tool execution process fully transparent to users.
The design philosophy of this system can be summarized in one sentence: Make the right thing easy, and the dangerous thing hard. buildTool()'s fail-closed defaults make "forgetting to declare safety properties" a safe mistake; layered budgets make "tools returning too much data" a controllable degradation; conditional loading makes "adding experimental tools" a zero-risk operation.
Tool invocation and orchestration — including the complete permission checking flow, concurrency execution scheduling strategy, and streaming progress propagation mechanism — will be covered in detail in Chapter 4.