Have you ever wondered: when you tell Claude Code “help me find where this function is defined,” how does it know to use GrepTool for searching instead of BashTool’s grep command?

Or when you say “look at this file,” why does it sometimes use FileReadTool directly, and sometimes first use GlobTool to list the directory before deciding which to read?

This isn’t random choice—it’s a precise tool system at work. Today we’re going to拆开 this system and see how 40+ tools “grow” onto an AI.

Tool System Overview The diagram: The tool system is like a LEGO factory, from parts to assembly to quality control

Tool Interface: The “Universal Language” of All Tools

Whether you’re using the built-in BashTool or a third-party tool via the MCP protocol, they all must satisfy the same TypeScript interface. This interface is defined in Tool.ts and is the foundation of the entire tool system.

A tool looks like this:

interface Tool<Input, Output> {
  readonly name: string;                    // Unique identifier
  description: (input, options) => Promise<string>;  // Manual for the model
  prompt: (options) => Promise<string>;      // System prompt fragment
  inputSchema: z.ZodType;                   // Parameter structure definition
  call: (args, context, canUseTool, ...) => Promise<ToolResult>;  // Execution logic
  checkPermissions: (input, context) => Promise<PermissionResult>;  // Permission check
  maxResultSizeChars: number;               // Result size limit
  isConcurrencySafe: (input) => boolean;    // Whether concurrent execution is allowed
  isReadOnly: (input) => boolean;           // Whether read-only
  isEnabled: () => boolean;                 // Whether currently available
}

Three design aspects here are particularly noteworthy:

description is a function, not a string. The same tool might need different descriptions under different permission modes. For example, when users configure alwaysDeny rules to prohibit certain subcommands, the tool description can proactively tell the model “don’t try these operations,” avoiding invalid calls at the prompt level.

inputSchema is defined with Zod v4. This enables 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 won’t receive undefined parameters.

call receives the canUseTool callback. During tool execution, there might be recursive checks for sub-operation permissions. For example, when AgentTool launches a sub-Agent, it needs to check whether that sub-Agent has permission to use specific tools. Permission checks aren’t a one-time gate but continuous verification throughout execution.

Tool Interface Fields The diagram: 7 core fields of the Tool interface and their responsibilities

Rendering Contract: The Tool’s “Performance” in the Terminal

The tool interface also defines a set of rendering methods, constituting the complete lifecycle of a tool in the terminal UI:

renderToolUseMessage       // Displayed when tool is called
renderToolUseProgressMessage  // Displayed during tool execution
renderToolResultMessage    // Displayed after tool completes

Additionally, there are renderToolUseErrorMessage, renderToolUseRejectedMessage (permission denied), and renderGroupedToolUse (grouped display of parallel tools).

Note the signature of renderToolUseMessage:

renderToolUseMessage(
  input: Partial<z.infer<Input>>,  // Note: it's Partial!
  options: { theme: ThemeName; verbose: boolean }
): React.ReactNode

The reason input is Partial: the API returns tool parameters as JSON in streaming fashion, so only partial fields are available before JSON parsing completes. The UI must render even with incomplete parameters—users shouldn’t see a blank screen.

buildTool Factory: The Fail-Closed Philosophy

Each concrete tool doesn’t directly export a Tool object but is constructed via the buildTool factory function:

export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
  return {
    ...TOOL_DEFAULTS,
    userFacingName: () => def.name,
    ...def,
  } as BuiltTool<D>;
}

Runtime behavior is minimal—just object spreading. But the type layer precisely simulates the semantics: if the tool definition provides a method, use the tool definition’s version; otherwise, use the default.

TOOL_DEFAULTS follows a safety principle: when uncertain, assume the most dangerous scenario.

const TOOL_DEFAULTS = {
  isEnabled: () => true,           // Enabled by default
  isConcurrencySafe: () => false,  // Unsafe by default (fail-closed!)
  isReadOnly: () => false,         // Non-read-only by default (fail-closed!)
  isDestructive: () => false,      // Non-destructive by default
  checkPermissions: () => ({ behavior: 'allow' }),  // Delegated to common permission system
  toAutoClassifierInput: () => '', // Excluded from auto safety classification by default
};

The two most important defaults are isConcurrencySafe: false and isReadOnly: false. This means: if a new tool forgets to declare these two properties, the system automatically treats it as “might modify filesystem and cannot execute concurrently”—the most conservative, safest assumption.

The system only relaxes restrictions when the tool developer actively declares safety.

GrepTool vs BashTool: Comparing Safety Declarations

GrepTool explicitly overrides both defaults:

export const GrepTool = buildTool({
  name: GREP_TOOL_NAME,
  searchHint: 'search file contents with regex (ripgrep)',
  maxResultSizeChars: 20_000,
  isConcurrencySafe() { return true },  // Search is a safe concurrent operation
  isReadOnly() { return true },         // Search doesn't modify files
  // ...
});

Search operations are naturally read-only and concurrency-safe, so GrepTool confidently declares this.

In contrast, BashTool’s concurrency safety is conditional:

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 judged to be a read-only command. A git status can run concurrently, but git push cannot. This input-aware concurrency control achieves precise safety management.

Tool Registration Pipeline: Three-Tier Filtering

tools.ts is the assembly center for the tool pool. A tool goes through three tiers of filtering from definition to final availability:

Tier 1: Compile-time / Startup-time Conditional Loading

Many tools use Feature Flags for conditional loading:

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,
    ]
  : [];

The feature() function comes from bun:bundle and is evaluated at packaging time. Disabled tools won’t even appear in the final JavaScript bundle—this is dead code elimination more thorough than runtime if checks.

There are also environment variable-driven conditional loads:

const REPLTool =
  process.env.USER_TYPE === 'ant'
  ? require('./tools/REPLTool/REPLTool.js').REPLTool
  : null;

USER_TYPE === ‘ant’ marks special tools for Anthropic internal staff, unavailable in public releases. This is the A/B testing “staging area” pattern.

Tier 2: getAllBaseTools() Assembles the Base Tool Pool

This function collects all tools that passed Tier 1 filtering into an array—it’s the system’s “tool registry”:

export function getAllBaseTools(): Tools {
  return [
    AgentTool,
    TaskOutputTool,
    BashTool,
    ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
    FileReadTool,
    FileEditTool,
    FileWriteTool,
    // ... 30+ tools
    ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
  ];
}

Note the interesting condition hasEmbeddedSearchTools(): in Anthropic internal builds, bfs (fast find) and ugrep are embedded in the Bun binary, so shell’s find and grep are already aliased to these fast tools, making standalone GlobTool and GrepTool redundant.

Tier 3: getTools() Runtime Filtering

The final filtering layer performs three operations:

  • Permission denial filtering: Removes tools covered by alwaysDeny rules via filterToolsByDenyRules()
  • REPL mode hiding: When REPL mode is enabled, basic tools like Bash, Read, Edit are hidden
  • isEnabled() final check: Each tool’s isEnabled() method is the final switch

Three-Tier Filtering The diagram: Three-tier filtering flow from tool definition to availability

Simple Mode: “Simplifying” Tools

getTools() also supports a “simple mode” (CLAUDE_CODE_SIMPLE) that only exposes three core tools: Bash, FileRead, and FileEdit. This is useful in certain integration scenarios—reducing tool count decreases token consumption and reduces the model’s decision burden.

Sometimes, giving AI fewer choices makes it perform better.

MCP Tool Integration

The final tool pool is assembled by assembleToolPool():

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:

Built-in tools take priority: uniqBy keeps the first occurrence of each name, built-in tools are listed first, and in name conflicts, built-in tools win.

Sorted by name for stable prompt caching: Built-in and MCP tools are each sorted before concatenation (rather than mixed sorting), ensuring built-in tools appear as a “continuous prefix.” This cooperates with API server-side cache breakpoint design—if MCP tools were interspersed among built-in tools, any addition or removal of MCP tools would cause all downstream cache keys to invalidate.

Tool Result Size Budget

When a tool returns results, the system faces a core tension: the model needs to see complete information, but the context window is limited. Claude Code solves this with a two-tier budget.

Tier 1: Per-tool result limit maxResultSizeChars

Each tool declares its result size limit via maxResultSizeChars:

ToolmaxResultSizeCharsNotes
McpAuthTool10,000Auth results, small data
GrepTool20,000Search results need to be concise
BashTool30,000Shell output can be lengthy
GlobTool100,000File lists can be extensive
FileReadToolInfinityNever persisted (avoid loops)

FileReadTool’s Infinity is a special design—prevents circular references from Read→persist file→Read.

Tier 2: Per-message aggregate limit

When the model calls multiple tools in parallel within one turn, all results are sent as the same user message. MAX_TOOL_RESULTS_PER_MESSAGE_CHARS = 200,000 limits the total tool result size per message.

The two control layers complement each other: per-tool limits prevent single-point overflow, per-message limits prevent parallel call collective explosion.

Lazy Loading: Solution for Too Many Tools

When the tool count exceeds a threshold (especially after connecting many MCP tools), sending all tools’ complete schemas to the model consumes massive tokens. Claude Code solves this with Deferred Loading.

Tools marked shouldDefer: true only send the tool name in the initial prompt (defer_loading: true), not the complete parameter schema. The model must first call ToolSearchTool to search by keywords and get the tool’s complete definition before it can call these deferred tools.

Each tool’s searchHint field is designed for this—it provides a 3-10 word capability description to help ToolSearchTool with keyword matching. For example, GrepTool’s searchHint is 'search file contents with regex (ripgrep)'.

Tools marked alwaysLoad: true are never deferred—their complete schema always appears in the initial prompt. This applies to core tools the model must be able to call directly in the first round of dialogue.

Lessons from Tool System Design

Claude Code’s tool system design offers several patterns worth borrowing:

Fail-closed defaults: buildTool()’s defaults assume the most dangerous scenario, and tool developers must actively declare safety properties. This flips safety from “opt-in” to “opt-out.”

Layered budget control: Per-tool results have limits, per-message also has aggregate limits. Two control layers complement each other.

Input-aware properties: isConcurrencySafe(input) and isReadOnly(input) receive tool input, not global judgments. The same BashTool has completely different safety properties for ls versus rm.

Progressive rendering: Three-stage rendering (intent → progress → result) gives users visibility at every stage of tool execution.

Compile-time elimination vs runtime filtering: Feature Flags eliminate unused tool code at compile time, permission rules filter tool lists at runtime. The two mechanisms serve different purposes.

What This Means for You

If you’re using Claude Code, understanding the tool system helps you:

Understand why the AI sometimes seems “dumb.” Maybe the model didn’t get stupider, but the tools got filtered out (permission rules, isEnabled returning false, etc.).

Optimize tool usage strategy. If you’ve connected many MCP tools, consider which to mark for deferred loading to reduce initial token consumption.

Design better MCP tools. Tool descriptions are instructions for the model—clarify “when to use this tool,” not just “what this tool does.”

If you want to build your own AI Agent tool system, you can borrow:

  • Adopt fail-closed defaults
  • Set result size limits per tool
  • Implement input-aware property control
  • Distinguish compile-time conditions from runtime filtering

The tool system is an AI Agent’s “hands.” Claude Code’s hands, through carefully designed interface contracts, permission models, and Feature Flag guards, form an extensible, controllable, safe capability architecture. Understanding it lets you use these hands better, or even build your own.