← 全部文章

Claude Code 源码解读:工具系统全解析

Claude Code 的工具系统是其核心能力的载体,40+ 内置工具通过五层架构实现从注册到执行的完整链路。

更新于 2026.08.07

本文目录 12 节
Claude Code 源码解读:工具系统全解析封面

深入 Claude Code 源码:工具系统全解析

本文基于 Claude Code 源码(由 source map 还原)进行深度解读,涵盖工具的发现、注册、定义、调用、安全、超时、输出截断、错误处理等完整链路。

一、全景概览

Claude Code 的工具系统是其核心能力的载体——Bash、Read、Write、Edit、Grep、Agent 等 40+ 工具,构成了 LLM 与外部世界交互的桥梁。整个工具系统可以分为五个层次:

┌─────────────────────────────────────────────────┐
│                  LLM API Response               │
│              (tool_use blocks)                   │
├─────────────────────────────────────────────────┤
│          流式工具执行器 (StreamingToolExecutor)    │
│         —— 边流式接收,边并发执行                   │
├─────────────────────────────────────────────────┤
│          工具编排层 (toolOrchestration)            │
│         —— 并发安全分区,串行/并行调度              │
├─────────────────────────────────────────────────┤
│          单工具执行管线 (toolExecution)            │
│  Zod校验 → 权限检查 → Hooks → call() → 结果持久化 │
├─────────────────────────────────────────────────┤
│          工具注册层 (tools.ts + Tool.ts)           │
│         —— 静态注册、动态组装、特性门控              │
└─────────────────────────────────────────────────┘

二、工具定义:buildTool 工厂模式

2.1 Tool 类型接口

每个工具都必须满足 Tool<Input, Output, P> 泛型接口(定义在 src/Tool.ts):

export type Tool<
  Input extends AnyObject = AnyObject,
  Output = unknown,
  P extends ToolProgressData = ToolProgressData,
> = {
  // 身份标识
  readonly name: string
  aliases?: string[]           // 向后兼容的别名(如 KillShell -> TaskStop)
  searchHint?: string          // ToolSearch 关键词提示(3-10词)

  // 参数定义
  readonly inputSchema: Input  // Zod schema
  readonly inputJSONSchema?: ToolInputJSONSchema  // MCP 工具用 JSON Schema

  // 核心执行
  call(
    args: z.infer<Input>,
    context: ToolUseContext,
    canUseTool: CanUseToolFn,
    parentMessage: AssistantMessage,
    onProgress?: ToolCallProgress<P>,
  ): Promise<ToolResult<Output>>

  // 安全属性(fail-closed 设计)
  isConcurrencySafe(input: z.infer<Input>): boolean  // 默认 false
  isReadOnly(input: z.infer<Input>): boolean          // 默认 false
  isDestructive(input: z.infer<Input>): boolean       // 默认 false
  isEnabled(): boolean                                 // 默认 true

  // 权限与描述
  checkPermissions(input: z.infer<Input>, ctx: ToolUseContext):
    Promise<PermissionResult>
  description(input: z.infer<Input>, options: {...}): Promise<string>
  prompt(options: {...}): string   // 发送给模型的完整指令文本

  // 结果处理
  mapToolResultToToolResultBlockParam(...): ToolResultBlockParam
  maxResultSizeChars?: number      // 输出截断阈值
}

核心设计哲学是 fail-closed(默认关闭):isConcurrencySafe 默认 false(不可并发),isReadOnly 默认 false(假定会写入)。工具必须显式声明自己是安全的,而不是默认安全。

2.2 buildTool 工厂函数

所有工具通过 buildTool() 工厂函数创建,它将 TOOL_DEFAULTS 与工具定义合并:

const TOOL_DEFAULTS = {
  isEnabled: () => true,
  isConcurrencySafe: (_input?: unknown) => false, // fail-closed
  isReadOnly: (_input?: unknown) => false, // fail-closed
  isDestructive: (_input?: unknown) => false,
  checkPermissions: (input, _ctx?) => Promise.resolve({ behavior: 'allow', updatedInput: input }),
  toAutoClassifierInput: (_input?: unknown) => '',
  userFacingName: (_input?: unknown) => '',
};

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

一个典型的工具定义(以 Glob 为例)只需要覆盖必要的字段:

export const GlobTool = buildTool({
  name: 'Glob',
  searchHint: 'find files by name pattern or wildcard',
  inputSchema: lazySchema(() => z.object({
    pattern: z.string().describe('The glob pattern to match files against'),
    path: z.string().optional().describe('The directory to search in'),
  })),
  isReadOnly: () => true,      // 显式声明只读
  isConcurrencySafe: () => true, // 显式声明可并发
  async call(args, context, ...) {
    // 执行逻辑...
  },
  // ...
})

2.3 参数验证:Zod v4 + JSON Schema 双轨

工具的参数定义使用 Zod v4 schema(通过 lazySchema() 惰性初始化并缓存),在需要发送给 API 时通过 zodToJsonSchema 转换为 JSON Schema。这保证了:

  1. 运行时类型安全:tool.inputSchema.safeParse(input) 在执行前校验模型输出
  2. API 兼容性:JSON Schema 格式符合 Anthropic API 的 tool 定义规范
  3. 缓存友好:WeakMap 按 ZodType 实例缓存转换结果,避免重复计算

三、工具发现与注册

3.1 静态注册中心:getAllBaseTools()

所有工具的注册发生在 src/tools.ts 的 getAllBaseTools() 函数中。这是一个显式的静态数组——每个工具都被明确导入和列出:

export function getAllBaseTools(): Tools {
  return [
    AgentTool,
    TaskOutputTool,
    BashTool,
    // 内嵌搜索工具时,Glob/Grep 由 shell alias 提供
    ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
    ExitPlanModeV2Tool,
    FileReadTool,
    FileEditTool,
    FileWriteTool,
    NotebookEditTool,
    WebFetchTool,
    TodoWriteTool,
    WebSearchTool,
    TaskStopTool,
    AskUserQuestionTool,
    SkillTool,
    EnterPlanModeTool,
    // ---- 条件注册:特性门控 ----
    ...(process.env.USER_TYPE === 'ant' ? [ConfigTool] : []),
    ...(isWorktreeModeEnabled() ? [EnterWorktreeTool, ExitWorktreeTool] : []),
    ...(WorkflowTool ? [WorkflowTool] : []),
    ...(MonitorTool ? [MonitorTool] : []),
    ...(getPowerShellTool() ? [getPowerShellTool()] : []),
    ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
    // ... 更多条件工具
  ];
}

这里有几个设计决策:

1. 显式优于隐式:不像某些框架通过目录扫描自动注册,Claude Code 选择手动列出每一个工具。这保证了:

  • 编译时就能检查工具是否存在
  • 工具的加载顺序确定且可控
  • 不会意外暴露未完成的工具

2. 特性门控 (Feature Gates):通过 feature() 函数和环境变量控制工具可用性:

  • process.env.USER_TYPE === 'ant' —— Anthropic 内部专用工具(ConfigTool、REPLTool)
  • isWorktreeModeEnabled() —— Git Worktree 隔离工具
  • isToolSearchEnabledOptimistic() —— ToolSearch 延迟加载工具

3. 循环依赖打破:某些工具(如 TeamCreateTool、SendMessageTool)通过 getter 函数延迟加载,避免模块间的循环引用。

3.2 工具过滤管线

从 getAllBaseTools() 到模型实际看到的工具列表,经过多层过滤:

getAllBaseTools()          全量工具(不考虑环境)
       │
       ▼
getTools(permContext)     过滤:isEnabled() + deny规则 + REPL模式 + simple模式
       │
       ▼
assembleToolPool()        合并 MCP 工具 + 去重(内置优先)+ 排序(prompt cache 稳定性)
       │
       ▼
filterToolsByDenyRules()  移除被 blanket deny 规则覆盖的工具
       │
       ▼
模型看到的工具列表

assembleToolPool() 中的去重策略值得注意:内置工具名优先于 MCP 工具。这意味着用户无法通过 MCP 覆盖核心工具(如 Bash、Read),保证了安全性。

排序也很有讲究:工具列表按照固定顺序排列以保持 prompt cache 稳定性——相同的工具集合总是产生相同的顺序,这样即使工具列表没有变化,API 的 prefix cache 也能命中。

3.3 延迟加载与 ToolSearch

当 ToolSearch 启用时,部分工具标记为 shouldDefer: true,不会随初始请求发送给模型。这些工具包括:TodoWrite、WebFetch、CronCreate、Monitor、Workflow 等。

模型如果需要使用这些延迟工具,必须先调用 ToolSearch 并传入 select:ToolName,触发工具的 schema 加载。这样做的好处是:

  • 减少初始 prompt 长度,节省 token
  • 模型在不需要某些工具时不会被其 schema 干扰
  • 适合工具数量很多的场景(如大量 MCP 工具)

四、工具执行管线

4.1 入口:runToolUse()

当模型返回 tool_use block 时,runToolUse() 异步生成器(src/services/tools/toolExecution.ts)是执行的入口:

export async function* runToolUse(
  toolUse: ToolUseBlock,
  assistantMessage: AssistantMessage,
  canUseTool: CanUseToolFn,
  toolUseContext: ToolUseContext,
): AsyncGenerator<MessageUpdateLazy, void> {
  // 1. 工具查找(支持别名回退)
  let tool = findToolByName(toolUseContext.options.tools, toolName)
  if (!tool) {
    const fallbackTool = findToolByName(getAllBaseTools(), toolName)
    if (fallbackTool && fallbackTool.aliases?.includes(toolName)) {
      tool = fallbackTool  // 旧名称向后兼容
    }
  }

  // 2. 中止检查
  if (toolUseContext.abortController.signal.aborted) {
    yield cancelMessage()
    return
  }

  // 3. 权限检查 + 执行
  yield* streamedCheckPermissionsAndCallTool(...)
}

4.2 核心管线:checkPermissionsAndCallTool()

这是整个工具系统最关键的函数,包含了工具调用的完整生命周期:

①  Zod Schema 校验
       │  tool.inputSchema.safeParse(input)
       │  失败 → 格式化 Zod 错误 → 返回给模型
       ▼
②  输入清洗
       │  剥离内部字段(如 _simulatedSedEdit)
       ▼
③  输入回填
       │  backfillObservableInput() 补充派生字段
       ▼
④  投机分类器(仅 Bash)
       │  提前启动后台权限分类器,为后续权限检查预热
       ▼
⑤  PreToolUse Hooks
       │  runPreToolUseHooks() 执行用户配置的钩子
       │  钩子可以 allow / deny / 修改输入 / 停止执行
       ▼
⑥  权限解析
       │  resolveHookPermissionDecision()
       │  = hook 结果 + checkRuleBasedPermissions() + canUseTool()
       ▼
⑦  tool.call() 执行
       │  传入:处理后的 input、context、canUseTool、progress 回调
       ▼
⑧  结果映射
       │  mapToolResultToToolResultBlockParam() 转为 API 格式
       ▼
⑨  PostToolUse Hooks
       │  runPostToolUseHooks() 执行后置钩子
       ▼
⑩  大结果持久化
       │  超阈值 → 写入磁盘 + 2KB 预览返回给模型

整个管线是异步生成器(AsyncGenerator),这意味着它可以在执行的任何阶段 yield 进度消息给 UI,实现”边执行边展示”的流式体验。

4.3 并发编排:partitionToolCalls()

当一次 API 响应中包含多个 tool_use block 时,Claude Code 不会简单地串行执行所有工具,而是使用智能分区策略:

function partitionToolCalls(
  toolUseMessages: ToolUseBlock[],
  toolUseContext: ToolUseContext,
): Batch[] {
  return toolUseMessages.reduce((acc: Batch[], toolUse) => {
    const tool = findToolByName(toolUseContext.options.tools, toolUse.name);
    const isConcurrencySafe = parsedInput?.success
      ? Boolean(tool?.isConcurrencySafe(parsedInput.data))
      : false;

    if (isConcurrencySafe && acc[acc.length - 1]?.isConcurrencySafe) {
      // 连续的并发安全工具合并为一个批次
      acc[acc.length - 1]!.blocks.push(toolUse);
    } else {
      // 非安全工具独占一个批次
      acc.push({ isConcurrencySafe, blocks: [toolUse] });
    }
    return acc;
  }, []);
}

编排规则非常直观:

输入: [Read A, Read B, Bash(cmd), Read C, Write D, Glob E]
分区: [Read A + Read B] → [Bash(cmd)] → [Read C] → [Write D] → [Glob E]
执行:  并行读取           串行执行      串行读取    串行写入    串行搜索

注:Read C 不能与前面的 Bash 合并,因为 Bash 不是并发安全的

并发上限通过环境变量 CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 控制,默认为 10。

4.4 流式执行器:StreamingToolExecutor

StreamingToolExecutor(src/services/tools/StreamingToolExecutor.ts)是 Claude Code 的一个精妙设计——它允许在 API 响应还在流式传输时就开始执行工具:

export class StreamingToolExecutor {
  private tools: TrackedTool[] = []
  private hasErrored = false
  private siblingAbortController: AbortController
  private discarded = false

  constructor(
    private readonly toolDefinitions: Tools,
    private readonly canUseTool: CanUseToolFn,
    toolUseContext: ToolUseContext,
  ) {
    this.siblingAbortController = createChildAbortController(
      toolUseContext.abortController,
    )
  }

  addTool(block: ToolUseBlock, assistantMessage: AssistantMessage): void {
    // 解析并判断是否可并发
    const parsedInput = toolDefinition.inputSchema.safeParse(block.input)
    const isConcurrencySafe = parsedInput?.success
      ? Boolean(toolDefinition.isConcurrencySafe(parsedInput.data))
      : false

    this.tools.push({ id: block.id, block, isConcurrencySafe, ... })
    // 立即开始处理队列
    this.processQueue()
  }
}

它的特性包括:

1. 流式即执行:工具在 API 响应流中出现的瞬间就被加入执行队列,不必等待整个响应完成。对于一个包含 5 个工具调用的响应,第一个工具可能在最后一个工具被接收之前就已经执行完毕。

2. 结果顺序保证:尽管执行是并发的,结果按工具在响应中出现的顺序返回。这是通过缓冲区实现的——已完成但非队首的结果会被暂存,等队首结果就绪后一起释放。

3. 兄弟错误级联:如果 Bash 工具执行出错,所有兄弟工具会被立即中止:

// Bash 工具错误时中止兄弟工具
private siblingAbortController: AbortController

// 如果 Bash 执行失败
if (this.hasErrored) {
  // 新加入的工具直接标记为错误,不再执行
  this.tools.push({ status: 'completed', results: [errorResult] })
  return
}

这是因为 Bash 命令之间往往有隐式依赖(如先 mkdir 再 write),一个失败意味着后续操作的前提条件可能不成立。

4. 可丢弃 (discard):当流式请求失败需要 fallback 到非流式重试时,调用 discard() 放弃所有待执行和进行中的工具:

discard(): void {
  this.discarded = true
}

五、权限与安全系统

这是 Claude Code 工具系统中最复杂、也最重要的部分。权限系统的目标是:在不阻碍正常使用的前提下,防止模型执行危险操作。

5.1 权限模式

Claude Code 支持多种权限模式,通过 PermissionMode 定义:

模式行为典型场景
default未授权工具需用户确认日常交互使用
acceptEdits自动允许文件编辑和文件系统命令知道自己在做什么的开发者
bypassPermissions跳过大多数权限提示CI/CD、自动化(--dangerously-skip-permissions)
dontAsk将所有 ask 转为 deny无人值守的后台 Agent
plan只读探索模式/plan 命令
autoAI 分类器自动审批安全操作Anthropic 内部特性

5.2 权限检查管线

hasPermissionsToUseTool() 函数(src/utils/permissions/permissions.ts)实现了多步权限检查管线:

// 核心权限检查管线(简化版)
async function hasPermissionsToUseToolInner(tool, input, context) {
  // ── Step 1: 规则优先(不可被模式覆盖)──
  // 1a. 整个工具被 deny 规则拒绝 → 立即拒绝
  const denyRule = getDenyRuleForTool(tool.name, denyRules);
  if (denyRule) return { behavior: 'deny' };

  // 1b. 整个工具有 ask 规则 → 需要确认
  const askRule = getAskRuleForTool(tool.name, askRules);
  if (askRule) return { behavior: 'ask' };

  // 1c. 工具自身的权限检查(如 Bash 检查子命令)
  const toolResult = await tool.checkPermissions(input, context);

  // 1d. 工具自身拒绝 → 拒绝
  if (toolResult.behavior === 'deny') return toolResult;

  // 1e. 需要用户交互的工具 → 即使在 bypass 模式也需确认
  if (tool.requiresUserInteraction?.()) return { behavior: 'ask' };

  // 1f. 内容级别的 ask 规则(如 Bash(npm publish:*))→ bypass 无效
  if (toolResult.behavior === 'ask') return toolResult;

  // 1g. 安全检查(.git/, .claude/ 等敏感路径)→ bypass 免疫
  if (isSafetyCheck(tool, input)) return { behavior: 'ask' };

  // ── Step 2: 模式检查(在规则之后)──
  // 2a. bypass 模式 → 允许
  if (mode === 'bypassPermissions') return { behavior: 'allow' };

  // 2b. always-allow 规则 → 允许
  if (matchesAlwaysAllowRules(tool, input)) return { behavior: 'allow' };

  // ── Step 3: 默认 → ask ──
  return { behavior: 'ask' };
}

关键设计原则:规则优先于模式。即使在 bypassPermissions 模式下:

  • deny 规则仍然生效
  • 敏感路径(.git/、.claude/、shell 配置文件)仍然需要确认
  • 需要用户交互的工具(AskUserQuestion)仍然会弹出提示

5.3 Bash 工具的深度安全分析

Bash 工具是 Claude Code 中权限检查最复杂的工具。它使用 tree-sitter AST 解析 对命令进行深度分析(src/tools/BashTool/bashSecurity.ts),检测 23+ 种安全风险:

// bashSecurity.ts 中的部分安全检查类别:
// 1. 命令替换:$() 和反引号
// 2. 进程替换:<() 和 >()
// 3. IFS 注入攻击
// 4. /proc 文件系统访问
// 5. 畸形 token 注入
// 6. 花括号展开攻击
// 7. 控制字符注入
// 8. Unicode 空白字符欺骗
// 9. 危险的 sed 模式
// 10. Git 提交替换
// ...

Bash 的权限检查还涉及子命令拆分:复合命令(如 git add . && git commit -m "msg" && git push)会被拆分为单独的子命令,每个子命令独立匹配权限规则。这允许用户精确控制:

// .claude/settings.json
{
  "permissions": {
    "allow": ["Bash(git commit:*)", "Bash(git status:*)"],
    "ask": ["Bash(git push:*)"],
    "deny": ["Bash(rm -rf /:*)"]
  }
}

5.4 沙箱系统

Claude Code 集成了平台级沙箱(通过 @anthropic-ai/sandbox-runtime),在操作系统层面限制工具的权限:

平台沙箱实现
macOSsandbox-exec
Linuxbubblewrap
WSL2原生支持

沙箱配置(SandboxSettingsSchema)包含:

{
  enabled: true,                    // 总开关
  failIfUnavailable: false,         // 沙箱不可用时是否硬性退出
  autoAllowBashIfSandboxed: true,   // 沙箱内的 Bash 自动允许
  allowUnsandboxedCommands: true,   // 允许 dangerouslyDisableSandbox
  excludedCommands: [],             // 豁免命令(非安全边界)
  network: {
    allowedDomains: [],             // 域名白名单
    denyRead: [], denyWrite: [],    // 文件系统网络限制
  }
}

当 Bash 工具设置了 dangerouslyDisableSandbox: true 时,命令会绕过沙箱执行。但这只有在 allowUnsandboxedCommands 配置为 true 时才被允许——并且此操作会在权限决策中留下 sandboxOverride 记录。

沙箱还有一些不可覆盖的安全硬限制:

  • 始终拒绝写入 settings.json 文件
  • 始终拒绝写入 .claude/skills 目录
  • 清理被植入的 bare-repo git 文件(防止 core.fsmonitor 逃逸攻击)

5.5 Hooks 机制

Hooks 是用户自定义的 shell 命令,可以在工具执行的各个阶段拦截:

// .claude/settings.json 中的 hooks 配置
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": ["./scripts/check-bash-safety.sh"]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": ["./scripts/lint-on-write.sh"]
      }
    ]
  }
}

Hook 的返回值可以影响权限决策:

  • {permissionDecision: "allow"} — 跳过权限提示
  • {permissionDecision: "deny"} — 拒绝执行
  • {decision: "approve"} / {decision: "block"} — 批准或阻断
  • {updatedInput: {...}} — 修改工具输入后继续

5.6 Auto 模式与 AI 分类器

Auto 模式是 Claude Code 最大胆的安全创新——用一个 AI 分类器来判断工具调用是否安全:

// auto 模式的权限决策流程
if (mode === 'auto' && result.behavior === 'ask') {
  // 快速路径 1: acceptEdits 模式已允许的操作 → 直接放行
  if (acceptEditsWouldAllow(tool, input)) return { behavior: 'allow' };

  // 快速路径 2: 安全工具白名单 → 直接放行
  if (isSafeToolAllowlist(tool.name)) return { behavior: 'allow' };

  // 慢速路径: 调用 AI 分类器
  const classifierResult = await classifyYoloAction(tool, input, context);
  if (classifierResult.shouldBlock) {
    // 记录拒绝,检查连续拒绝次数
    recordDenial(classifierResult);
    return { behavior: 'deny' };
  }
  return { behavior: 'allow' };
}

分类器有自己的拒绝追踪机制:当连续拒绝次数超过阈值(DENIAL_LIMITS),系统会回退到交互式权限提示,避免无限循环的分类器拒绝。

Auto 模式还有一项安全设计:启动时会剥离危险的通配符权限规则(如 Bash(*)、Agent(*)),因为这些规则会让分类器形同虚设。退出 auto 模式时再恢复这些规则。

六、超时机制

6.1 Bash 超时

Bash 工具是唯一有显式超时机制的工具(定义在 src/utils/timeouts.ts):

const DEFAULT_TIMEOUT_MS = 120_000; // 2 分钟
const MAX_TIMEOUT_MS = 600_000; // 10 分钟

export function getDefaultBashTimeoutMs(env = process.env): number {
  const envValue = env.BASH_DEFAULT_TIMEOUT_MS;
  if (envValue) {
    const parsed = parseInt(envValue, 10);
    if (!isNaN(parsed) && parsed > 0) return parsed;
  }
  return DEFAULT_TIMEOUT_MS;
}

export function getMaxBashTimeoutMs(env = process.env): number {
  const envValue = env.BASH_MAX_TIMEOUT_MS;
  if (envValue) {
    const parsed = parseInt(envValue, 10);
    if (!isNaN(parsed) && parsed > 0) return Math.max(parsed, getDefaultBashTimeoutMs(env));
  }
  return Math.max(MAX_TIMEOUT_MS, getDefaultBashTimeoutMs(env));
}

超时由模型在调用时通过 timeout 参数指定,并经过上下限钳制:

// 模型调用
{ "name": "Bash", "input": { "command": "npm test", "timeout": 300000 } }

// 系统处理
const effectiveTimeout = Math.min(
  Math.max(modelTimeout, 0),
  getMaxBashTimeoutMs()  // 不超过 10 分钟
)

超时后的自动后台化:当启用此特性时,超时的 Bash 命令不会被直接杀死,而是转入后台继续运行。模型可以通过 TaskOutput 工具查询后台任务的状态。

6.2 其他超时

除了 Bash 工具的显式超时,系统各处还有多种超时机制:

  • MCP 连接超时:5-15 秒(通过 AbortSignal.timeout())
  • 文件建议超时:10 秒
  • Session 超时:24 小时(DEFAULT_SESSION_TIMEOUT_MS)
  • 分类器投机检查:2 秒宽限期(与用户交互竞速)

七、输出截断:两级防护

大型工具输出如果不加控制,会迅速耗尽模型的上下文窗口。Claude Code 实现了两级截断系统。

7.1 第一级:单工具持久化阈值

每个工具可以声明 maxResultSizeChars,超过此阈值的输出会被持久化到磁盘:

// src/utils/toolResultStorage.ts
export function getPersistenceThreshold(
  toolName: string,
  declaredMaxResultSizeChars: number,
): number {
  // Infinity = 硬性退出(如 Read 工具自限制 token 数)
  if (!Number.isFinite(declaredMaxResultSizeChars)) {
    return declaredMaxResultSizeChars;
  }
  // GrowthBook 远程配置覆盖(最高优先级)
  const overrides = getFeatureValue_CACHED_MAY_BE_STALE(PERSIST_THRESHOLD_OVERRIDE_FLAG, {});
  const override = overrides?.[toolName];
  if (typeof override === 'number' && override > 0) return override;

  // 工具声明值与全局默认值取较小者
  return Math.min(declaredMaxResultSizeChars, DEFAULT_MAX_RESULT_SIZE_CHARS);
}

持久化后的结果格式:

<project>/.claude/<sessionId>/tool-results/<toolUseId>.txt   ← 完整输出

模型收到的是一个 2KB 预览,包裹在 <persisted-output> 标签中:

<persisted-output>
  工具输出的前 2000 个字符...

  [输出已持久化到: /path/to/tool-results/abc123.txt]
  [使用 Read 工具读取完整输出]
</persisted-output>

各工具的默认阈值:

工具maxResultSizeChars说明
Glob100,000文件列表可能很长
Bash默认值(50K)可通过常量调整
ReadInfinity自限制(max 25K tokens),避免循环读取
Grep默认值(50K)搜索结果
全局默认50,000DEFAULT_MAX_RESULT_SIZE_CHARS

Read 工具使用 Infinity 是一个巧妙的设计——如果 Read 的输出也被持久化到文件,模型就需要再次用 Read 去读取持久化文件,形成无限递归。所以 Read 工具通过自身限制输出 token 数(最多 25,000 tokens)来解决这个问题。

7.2 第二级:单消息聚合预算

即使每个工具的输出都在阈值内,一次 API 响应中多个工具的输出总和仍然可能很大。enforceToolResultBudget() 函数负责这个层面的控制:

// 单条用户消息中所有 tool_result 的总字符数上限
const MAX_TOOL_RESULT_PER_MESSAGE_CHARS = 200_000;

export function enforceToolResultBudget(messages: Message[]): Message[] {
  // 检查每条用户消息中 tool_result 的总大小
  // 超出预算时,将最大的结果持久化到磁盘
  // 使用 ContentReplacementState 跟踪替换状态,保证跨轮次一致性
}

这个两级系统确保了:

  • 单个工具输出不超过 50K(或工具自定义值)
  • 单条消息中所有工具输出总计不超过 200K
  • 超出的内容可通过 Read 工具按需读取

八、错误处理

8.1 错误格式化

工具执行错误通过 formatError() 函数(src/utils/toolErrors.ts)统一格式化:

export function formatError(error: unknown): string {
  // AbortError → 用户中断消息
  if (error instanceof AbortError) {
    return error.message || INTERRUPT_MESSAGE_FOR_TOOL_USE;
  }
  if (!(error instanceof Error)) return String(error);

  // ShellError → 退出码 + stderr + stdout
  const parts = getErrorParts(error);
  // ShellError: [Exit code 1, stderr内容, stdout内容]
  // 其他Error: [message, stderr?, stdout?]

  const fullMessage = parts.filter(Boolean).join('\n').trim() || 'Command failed with no output';

  // 超长错误截断:保留首尾各 5000 字符
  if (fullMessage.length <= 10000) return fullMessage;
  const start = fullMessage.slice(0, 5000);
  const end = fullMessage.slice(-5000);
  return `${start}\n\n... [${fullMessage.length - 10000} characters truncated] ...\n\n${end}`;
}

错误消息的截断策略是保留首尾(各 5000 字符),而非简单截断前 N 个字符。这是因为编译错误等信息往往在开头和结尾都有重要内容。

8.2 错误分类

classifyToolError() 函数对错误进行分类(主要用于遥测),提取不包含文件路径等敏感信息的安全错误名:

export function classifyToolError(error: unknown): string {
  if (error instanceof TelemetrySafeError) return error.safeMessage;
  if (error instanceof Error && 'code' in error && 'errno' in error) {
    return `Error:${error.code}`; // 如 Error:ENOENT, Error:EACCES
  }
  if (error instanceof Error && error.name) return error.name;
  return 'Error';
}

8.3 错误返回给模型

工具错误以 tool_result 块的形式返回给模型,带有 is_error: true 标记:

{
  type: 'tool_result',
  tool_use_id: 'toolu_abc123',
  is_error: true,
  content: '<tool_use_error>Error: ENOENT: no such file or directory</tool_use_error>'
}

这让模型能够理解错误并调整策略——比如文件不存在时换一个路径,或命令失败时检查参数。

8.4 PostToolUseFailure Hooks

工具执行失败后,runPostToolUseFailureHooks() 会运行用户配置的失败钩子,可以提供额外上下文或触发阻断:

// hooks 配置示例
{
  "hooks": {
    "PostToolUseFailure": [{
      "matcher": "Bash",
      "hooks": ["./scripts/on-bash-failure.sh"]
    }]
  }
}

九、重试机制

Claude Code 没有传统的工具级自动重试机制——工具执行失败就是失败,错误直接返回给模型。但系统在多个层面实现了重试能力:

9.1 API 级别重试

当 API 调用失败时(src/query.ts),系统支持模型降级重试:

// 当主模型返回 FallbackTriggeredError 时
if (error instanceof FallbackTriggeredError) {
  // 1. 切换到降级模型
  // 2. 丢弃当前流式结果
  // 3. 创建新的 StreamingToolExecutor
  // 4. 用降级模型重试整个请求
}

9.2 Prompt 过长重试

当 API 返回 413(prompt too long)错误时,系统执行被动压缩:

// 触发 reactive compact
// 1. 压缩上下文(总结历史消息)
// 2. 用压缩后的上下文重试请求

9.3 Hook 触发重试

权限拒绝的 Hook 可以返回 {retry: true},通知模型可以在 hook 修改后重试:

// toolExecution.ts
if (hookResult.retry) {
  // 返回特殊响应,告知模型可以重试
}

9.4 分类器拒绝回退

在 auto 模式下,连续分类器拒绝超过阈值后,系统会回退到交互式提示,而非无限重试。

十、MCP 工具集成

Claude Code 的工具系统天然支持 MCP(Model Context Protocol)外部工具。MCP 工具与内置工具共用同一套执行管线:

// assembleToolPool 中合并内置工具和 MCP 工具
export function assembleToolPool(permContext, mcpTools) {
  const builtInTools = getTools(permContext);
  // 去重:内置工具名优先(安全考虑)
  const mcpToolsFiltered = mcpTools.filter((mcp) => !builtInTools.some((b) => b.name === mcp.name));
  // 合并并按固定顺序排序(prompt cache 稳定性)
  return [...builtInTools, ...mcpToolsFiltered].sort(byName);
}

MCP 工具的特殊之处:

  • 使用 inputJSONSchema 而非 Zod schema(直接接收 JSON Schema)
  • 通过 mcp__serverName__toolName 命名空间避免冲突
  • 支持 defer_loading 延迟加载(与 ToolSearch 配合)
  • MCP 连接有自己的重试逻辑(SSETransport: 10 次重试)

十一、设计哲学总结

纵观 Claude Code 的工具系统,几个设计哲学贯穿始终:

1. Fail-closed(默认安全)

所有安全相关的属性默认为”不安全”:工具默认不可并发、默认不是只读、权限默认需要确认。必须显式声明才能获得更高的自由度。

2. 分层防御

安全不是一个单点,而是多层叠加:Zod 校验 → 工具自身检查 → 规则系统 → 权限模式 → 沙箱 → Hooks。每一层都可以独立拦截危险操作。

3. 规则优先于模式

即使在最宽松的 bypassPermissions 模式下,显式的 deny 规则和敏感路径保护仍然生效。这避免了”开发者为了方便开了 bypass 模式,然后忘记了安全边界”的场景。

4. 流式优先

从 AsyncGenerator 到 StreamingToolExecutor,系统在设计上就面向流式场景。工具执行不需要等待 API 响应完成,结果通过流式生成器实时传递给 UI。

5. 可观测性

每一个权限决策都有 decisionReason(包含类型、模式、规则来源),每一个工具执行都有遥测事件。这让调试和审计成为可能。

6. 向后兼容

工具通过 aliases 支持旧名称,通过 fallbackTool 机制在所有已注册工具中查找。这保证了旧的 transcript 和配置文件不会因为工具重命名而失效。


源码参考:本文所有代码片段均来自 Claude Code 源码(由 source map 还原),关键文件包括:

  • src/Tool.ts — 工具类型定义与 buildTool 工厂
  • src/tools.ts — 工具注册中心
  • src/services/tools/toolExecution.ts — 工具执行管线
  • src/services/tools/toolOrchestration.ts — 并发编排
  • src/services/tools/StreamingToolExecutor.ts — 流式执行器
  • src/utils/permissions/permissions.ts — 权限系统核心
  • src/tools/BashTool/bashSecurity.ts — Bash 安全分析
  • src/utils/toolResultStorage.ts — 输出截断与持久化
  • src/utils/timeouts.ts — 超时配置
  • src/utils/toolErrors.ts — 错误格式化

评论