Claude Code 源码分析第 02 课 · 第 02
第 02 课

工具系统

工具接口 · 注册装配 · 路由分发 · 权限门控 · 执行与结果回流

概览

Claude Code 暴露给模型的每项能力,本质上都是一个 Tool:读写文件、执行 Bash、搜索网页、调用 MCP 服务,都是同一个协议下的不同实现。工具系统负责把“模型想做什么”变成“机器实际执行什么”。

这一层不只是函数注册表。它同时承担输入校验、权限判断、并发调度、结果序列化和进度回传,是 Claude Code 能安全落地副作用的核心控制面。

涵盖的文件: Tool.ts, tools.ts, tools/utils.ts, services/tools/toolOrchestration.ts, services/tools/toolExecution.ts, services/tools/StreamingToolExecutor.ts

架构:从界面到结果

单个工具调用的完整生命周期遵循以下路径:

flowchart TD A["Tool Interface
(Tool.ts)
name · inputSchema · call · checkPermissions
isConcurrencySafe · isReadOnly · isDestructive"] -->|"buildTool() fills defaults"| B B["Registration
(tools.ts)
getAllBaseTools() → getTools() → assembleToolPool()
Feature flags · env vars · deny-rule filtering"] -->|"readonly Tool[]"| C C["Routing / Dispatch
(toolExecution.ts: runToolUse)
findToolByName() · alias fallback
abort-before-start check"] -->|"tool found"| D D["Input Validation
(toolExecution.ts)
Zod safeParse → schema errors
tool.validateInput() → semantic errors
backfillObservableInput cloning"] -->|"parsedInput"| E E["Permission Gate
(toolExecution.ts: checkPermissions­AndCallTool)
PreToolUse hooks → hookPermissionResult
canUseTool() · alwaysAllow / alwaysDeny rules
Interactive prompt / auto-classifier"] -->|"behavior: allow"| F F["Execution
(tool.call)
async generator · onProgress callback
ToolResult<Output> + contextModifier
PostToolUse hooks"] -->|"ToolResult"| G G["Result Processing
(toolExecution.ts)
mapToolResultToToolResultBlockParam
processToolResultBlock (size budget)
yield MessageUpdateLazy"] -->|"MessageUpdate"| H H["Orchestration
(toolOrchestration.ts / StreamingToolExecutor)
partitionToolCalls · concurrent vs serial
sibling-abort on Bash error
in-order result emission"] style A fill:#22201d,stroke:#7d9ab8,color:#b8b0a4 style B fill:#22201d,stroke:#7d9ab8,color:#b8b0a4 style C fill:#1a1816,stroke:#8e82ad,color:#b8b0a4 style D fill:#1a1816,stroke:#8e82ad,color:#b8b0a4 style E fill:#231f16,stroke:#b8965e,color:#b8b0a4 style F fill:#1d211b,stroke:#6e9468,color:#b8b0a4 style G fill:#1a1816,stroke:#8e82ad,color:#b8b0a4 style H fill:#22201d,stroke:#c47a50,color:#b8b0a4

1. 工具接口(Tool.ts)

The Tool<Input, Output, P> 类型是一个 协议合同 — 每个工具都必须满足的 TypeScript 结构类型。 它具有三个参数的通用性:Zod 输入模式、输出类型和进度事件形状。

核心所需成员
export type Tool<
  Input extends AnyObject,
  Output,
  P extends ToolProgressData
> = {
  name: string                 // primary identifier the model uses
  aliases?: string[]          // legacy names for backward compat
  inputSchema: Input          // Zod schema — source of truth for validation
  maxResultSizeChars: number  // overflow → persist to disk

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

  checkPermissions(
    input: z.infer<Input>,
    context: ToolUseContext
  ): Promise<PermissionResult>

  isConcurrencySafe(input: z.infer<Input>): boolean
  isReadOnly(input: z.infer<Input>): boolean
  isDestructive?(input: z.infer<Input>): boolean
}
buildTool() — 工厂函数

不是让作者提供每一种方法, buildTool() 合并一个 ToolDef (部分)具有安全默认值。 默认值是 fail-closed:假设写入,假设并发不安全,默认不拒绝任何内容(让通用权限系统决定)。

// Defaults — applied when a ToolDef omits the key
const TOOL_DEFAULTS = {
  isEnabled:         () => true,
  isConcurrencySafe: () => false,   // conservative: assume state mutation
  isReadOnly:        () => false,
  isDestructive:     () => false,
  // Defer to the general permission system by default
  checkPermissions:  (input) =>
    Promise.resolve({ behavior: 'allow', updatedInput: input }),
  toAutoClassifierInput: () => '',  // skip security classifier
  userFacingName:    () => '',
}

export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
  return {
    ...TOOL_DEFAULTS,
    userFacingName: () => def.name, // sensible fallback
    ...def,
  } as BuiltTool<D>
}
关键见解: TypeScript 的魔力 BuiltTool<D> 反映了类型级别的运行时分布,因此返回类型尽可能窄 - 保留定义中的文字类型。
ToolResult 和 contextModifier

一个工具的 call() 返回一个 ToolResult<T>:

export type ToolResult<T> = {
  data: T
  newMessages?: (UserMessage | AssistantMessage | ...)[]
  // Only honored for non-concurrency-safe tools
  contextModifier?: (context: ToolUseContext) => ToolUseContext
}

The contextModifier 是一种工具(例如, EnterPlanMode) 改变共享状态而不触及全局变量。 该工具完成后将连续应用它。 并发工具 cannot use contextModifier ——评论中 StreamingToolExecutor 明确指出这是一个已知的限制。

值得注意的可选方法:UI 和分类器

该界面具有较大的表面积用于渲染和安全集成:

  • renderToolUseMessage() — 在流式传输工具输入时显示反应节点
  • renderToolResultMessage() — 记录中结果的反应节点
  • renderGroupedToolUse() — 多个同类型工具一起运行时的批量渲染
  • toAutoClassifierInput() ——安全分类器的紧凑表示; 返回 '' 跳过
  • extractSearchText() — 用于转录搜索索引; 必须与渲染内容匹配,否则会出现幻影点击
  • interruptBehavior()'cancel' or 'block':当用户在此工具运行时键入时会发生什么
  • shouldDefer / alwaysLoad — ToolSearch 延迟加载标志

2.注册(tools.ts)

tools.ts真理的来源 存在哪些工具。 它实现了三层装配管道。

getAllBaseTools() — 详尽的目录

返回每个工具 could 在当前版本中可用。 功能标志和环境变量使用 Bun 在模块加载时控制条件工具 feature() 死代码消除:

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

const SleepTool = feature('PROACTIVE') || feature('KAIROS')
  ? require('./tools/SleepTool/SleepTool.js').SleepTool
  : null

export function getAllBaseTools(): Tools {
  return [
    AgentTool, TaskOutputTool, BashTool,
    ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
    FileReadTool, FileEditTool, FileWriteTool,
    WebFetchTool, TodoWriteTool, WebSearchTool,
    // ... 30+ more tools, conditionally included
    ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
  ]
}
缓存稳定性注意事项: 该列表与 Statsig 动态配置保持同步,以便系统提示工具列表在用户之间保持稳定 - 启用服务器端提示缓存。
getTools() — 按上下文过滤

在之上应用特定于模式的过滤 getAllBaseTools():

  1. 简单模式 (CLAUDE_CODE_SIMPLE) — 仅 Bash、阅读、编辑
  2. REPL 模式 — 隐藏原始工具; 他们住在 REPL VM 内
  3. 拒绝规则 — 过滤工具匹配 alwaysDenyRules 在权限上下文中
  4. isEnabled() — 每个工具都可以否决自己
assembleToolPool() — 内置 + MCP,为了缓存稳定性而排序
export function assembleToolPool(
  permissionContext: ToolPermissionContext,
  mcpTools: Tools,
): Tools {
  const builtInTools = getTools(permissionContext)
  const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)

  // Built-ins sorted alphabetically as a prefix, then MCP tools sorted alphabetically.
  // Keeps a stable cache breakpoint between the two groups.
  const byName = (a, b) => a.name.localeCompare(b.name)
  return uniqBy(
    [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
    'name',
  )
}
将内置工具和 MCP 工具排序为两个单独的字母组可保留服务器的缓存断点。 每当 MCP 工具在内置工具之间进行排序时,交错放置它们就会破坏缓存。

3.编排(toolOrchestration.ts)

当模型响应包含多个 tool_use 块,协调器决定 哪些是并发运行,哪些是串行运行 using partitionToolCalls().

分区算法

规则很简单但很强大:

  • 连续工具在哪里 isConcurrencySafe(input) === true 一起批处理并并行运行。
  • 任何非安全工具都会破坏批次并单独串行运行。
  • try/catch 换行 isConcurrencySafe() — 解析失败默认为 false (conservative).
// Simplified from partitionToolCalls()
for (const toolUse of toolUseMessages) {
  const safe = isConcurrencySafe(toolUse)
  if (safe && lastBatch?.isConcurrencySafe) {
    lastBatch.blocks.push(toolUse)       // extend parallel group
  } else {
    acc.push({ isConcurrencySafe: safe, blocks: [toolUse] })
  }
}

并发批次使用 all() 异步生成器组合器的并发上限为 CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY (默认 10)。

上下文突变:contextModifier 之舞

非安全工具可能会返回 contextModifier 变异 ToolUseContext (例如,更改权限模式)。 串行工具在下一个工具运行之前立即应用修改器。 并发工具将它们的修改器排队并在批处理完成后应用它们:

// Serial: apply immediately so next tool sees updated context
if (update.contextModifier) {
  currentContext = update.contextModifier.modifyContext(currentContext)
}

// Concurrent: queue, apply after batch
queuedContextModifiers[toolUseID].push(modifyContext)
// ... after all concurrent tools complete:
for (const modifier of modifiers) {
  currentContext = modifier(currentContext)
}

4. 流式执行(StreamingToolExecutor.ts)

StreamingToolExecutorreal-time 变体:在模型的完整响应完成之前,当工具的块从 API 流入时,它开始执行工具。 这是生产流模式中使用的类。

工具生命周期状态

每个工具都会跟踪一个 ToolStatus:

type ToolStatus = 'queued' | 'executing' | 'completed' | 'yielded'
  • queued — 收到块,等待并发槽
  • executingrunToolUse() 发电机正在消耗
  • completed — 收集的结果,尚未发送给调用者
  • yielded — 按顺序发出,完成

进度消息(type: 'progress')存储在 pendingProgress 并发出 immediately 无序——他们不需要等待结果。

并发防护:canExecuteTool()
private canExecuteTool(isConcurrencySafe: boolean): boolean {
  const executing = this.tools.filter(t => t.status === 'executing')
  return (
    executing.length === 0 ||
    (isConcurrencySafe && executing.every(t => t.isConcurrencySafe))
  )
}

非安全工具必须等待所有执行工具完成。 仅当所有当前正在执行的工具也是安全的时,安全工具才能加入。

兄弟中止:Bash 错误级联

执行人持有 siblingAbortController — 主中止控制器的子级。 当一个 重击工具 产生错误结果,它中止同级:

if (isErrorResult && tool.block.name === BASH_TOOL_NAME) {
  this.hasErrored = true
  this.erroredToolDescription = getToolDescription(tool)
  this.siblingAbortController.abort('sibling_error')
}
只有 Bash 错误会级联。 Read/WebFetch/etc 被视为独立的 — 一次失败不会影响并行读取。

每个工具 toolAbortController 冒泡非同级中止到主查询控制器——对于 ExitPlanMode 的“清除上下文 + 自动”流程至关重要。

使用 getRemainingResults() 按顺序发出结果

即使工具同时执行, 结果必须按照模型请求的顺序发出 (模型的 tool_result 消息按 ID 配对)。 执行器通过迭代来实现这一点 this.tools 仅当头部工具处于插入顺序时才屈服 completed:

for (const tool of this.tools) {
  // Progress always goes through immediately
  while (tool.pendingProgress.length > 0) {
    yield { message: tool.pendingProgress.shift()! }
  }
  if (tool.status === 'completed' && tool.results) {
    tool.status = 'yielded'
    for (const msg of tool.results) yield { message: msg }
  } else if (tool.status === 'executing' && !tool.isConcurrencySafe) {
    break  // head is a non-safe executing tool — must wait
  }
}

5. 工具执行管道(toolExecution.ts)

runToolUse() 是中央调度功能。 它处理未知工具、中止前检查并委托 streamedCheckPermissionsAndCallTool() 它将异步权限+执行流程包装在 Stream 多重进展和最终结果。

完整的 checkPermissionsAndCallTool() 流程
  1. Zod验证inputSchema.safeParse(input)。 失败返回一个 InputValidationError 工具立即得出结果。 包括未发送架构的延迟工具的提示。
  2. 语义验证tool.validateInput()。 自定义每个工具检查(路径遍历、文件大小限制等)。
  3. 推测分类器 — Bash 命令推测启动允许分类器 before 挂钩运行,因此分类器结果在权限对话框可能需要时已准备就绪。
  4. backfillObservableInput — 创建浅克隆并添加遗留/派生字段。 克隆是 hooks 和 canUseTool 看到的; 原始(API 绑定)输入永远不会改变。
  5. PreToolUse 挂钩 — 异步生成器; 可以产生进度、更新输入、设置权限结果或完全停止执行。
  6. canUseTool() — 主要权限门。 检查始终允许/始终拒绝规则、自动分类器,并可选择显示交互式权限提示。
  7. tool.call() — 使用进度回调实际执行。
  8. PostToolUse 挂钩 — 工具完成后运行。
  9. 结果序列化mapToolResultToToolResultBlockParam() + 尺寸预算处理。
输入突变安全:backfillObservableInput
// A shallow clone is made for hooks/canUseTool to observe
const backfilledClone =
  tool.backfillObservableInput && processedInput !== null
    ? ({ ...processedInput } as typeof processedInput)
    : null
if (backfilledClone) {
  tool.backfillObservableInput!(backfilledClone as Record<string, unknown>)
  processedInput = backfilledClone
}

原来的 parsedInput.datatool.call()。 原始版本的突变会改变转录序列化并破坏测试中的 VCR 固定哈希值。

纵深防御:_simulatedSedEdit 剥离

Bash 工具有一个内部 _simulatedSedEdit 用户批准后权限系统使用的字段。 如果模型以某种方式在其输出中提供了此内容,则代码会在执行前将其删除:

if (tool.name === BASH_TOOL_NAME && '_simulatedSedEdit' in processedInput) {
  const { _simulatedSedEdit: _, ...rest } = processedInput
  processedInput = rest  // field stripped, execution proceeds safely
}

这是一种纵深防御措施,尽管 Zod 的 strictObject 应该已经拒绝该字段。

通过 Stream 复用进度 + 结果

streamedCheckPermissionsAndCallTool() 将基于回调的进度 API 和基于生成器的结果 API 桥接到一个单一的 API 中 AsyncIterable<MessageUpdateLazy>:

const stream = new Stream<MessageUpdateLazy>()

checkPermissionsAndCallTool(..., progress => {
  // Progress callback → enqueue progress message to stream
  stream.enqueue({ message: createProgressMessage(...) })
})
  .then(results => {
    for (const r of results) stream.enqueue(r)
  })
  .finally(() => stream.done())

return stream  // AsyncIterable that yields progress then results

6. 权限上下文

The ToolPermissionContext (包裹在 DeepImmutable)流经整个系统。 它驱动注册时过滤和运行时权限检查。

Structure
export type ToolPermissionContext = DeepImmutable<{
  mode: PermissionMode             // 'default' | 'plan' | 'bypassPermissions' | ...
  additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>
  alwaysAllowRules: ToolPermissionRulesBySource
  alwaysDenyRules:  ToolPermissionRulesBySource
  alwaysAskRules:   ToolPermissionRulesBySource
  isBypassPermissionsModeAvailable: boolean
  shouldAvoidPermissionPrompts?: boolean  // background agents: auto-deny
  awaitAutomatedChecksBeforeDialog?: boolean
}>

DeepImmutable 防止任何代码路径意外地就地改变权限。 改变上下文的唯一方法是通过 contextModifier 从返回 ToolResult.

filterToolsByDenyRules()

在注册时(模型可见的工具列表)和 MCP 工具组装时应用。 用途 getDenyRuleForTool() 它理解 MCP 服务器前缀规则,例如 mcp__server 全面拒绝来自服务器的所有工具:

export function filterToolsByDenyRules(
  tools: readonly T[],
  permissionContext: ToolPermissionContext
): T[] {
  return tools.filter(tool => !getDenyRuleForTool(permissionContext, tool))
}

要点

1
协议,而不是类层次结构。 Tool 是 TypeScript 结构类型。 任何满足该接口的对象都是一个工具——内置的、MCP 或动态生成的。 buildTool() 填写安全的失败关闭默认值,以便作者仅覆盖他们需要的内容。
2
三层组装。 getAllBaseTools()getTools()assembleToolPool()。 在加载时对工具进行功能标记; 拒绝规则在模型看到它们之前过滤它们; isEnabled() 是最后的否决权。 MCP 工具被分类为单独的字母后缀,以保持提示缓存稳定性。
3
并发是数据驱动的。 isConcurrencySafe(input) 称为“按工具调用”,而不是“按工具类型”。 正在运行的 Bash 工具 ls 跑步时可能是安全的 rm -rf 不是。 编排器在运行时将工具块划分为并发/串行批次。
4
不可变的上下文,功能突变。 ToolPermissionContext is DeepImmutable。 状态变化通过以下方式发生 contextModifier 返回的函数 ToolResult,在工具完成后应用。 这可以防止意外的交叉工具状态污染。
5
输入突变受到仔细控制。 该代码维护三个不同的输入副本:API 绑定的原始副本(用于缓存)、回填的可观察克隆(用于 hooks/canUseTool)以及潜在的钩子更新的调用输入。 每个边界都是有意为之并记录在案的。
6
巴什很特别。 只有 Bash 错误通过以下方式级联到同级工具 siblingAbortController。 Bash 在钩子运行之前推测性地启动安全分类器。 巴什有 _simulatedSedEdit 防守被剥夺的内场。 Bash 的隐式依赖链证明将其与纯读取工具区别对待是合理的。

知识检查

Q1. buildTool() 默认把 isConcurrencySafe 设成什么,为什么?
正确! 工具默认是 fail-closed 的。框架会先假设它可能写状态、可能不适合并发,然后由具体工具主动声明自己可以更宽松地运行。
Q2. 为什么内置工具和 MCP 工具要分别排序,而不是混在一起做一次总排序?
正确! 源码里明确提到,内置工具是一段稳定前缀,MCP 工具是另一段后缀。把两组分开排序,可以保住服务器侧提示缓存的边界。
Q3. 为什么只有 Bash 错误会触发 sibling abort,而读文件、抓网页这类错误不会?
正确! Bash 工具之间经常有顺序依赖,例如先建目录再写文件;而并行读取失败通常不该拖死其它读取任务,所以只有 Bash 错误会级联中止。
Q4. backfillObservableInput() 为什么要改浅拷贝,而不是直接改 parsedInput.data
正确! 原始输入既参与转录,也影响缓存和测试快照。框架只让 hook 与权限系统观察 clone,避免把这些派生字段写回真正的 API 输入。
Q5. 如果并发批次中的某个工具返回了 contextModifier,它会在什么时候生效?
正确! 串行工具可以立即改上下文,并发工具不行。并发批次里的修改器会先排队,等整批结束后再按顺序落地,避免互相踩状态。
0/5