工具系统
概览
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
架构:从界面到结果
单个工具调用的完整生命周期遵循以下路径:
(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: checkPermissionsAndCallTool)
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>
}
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] : []),
]
}
getTools() — 按上下文过滤
在之上应用特定于模式的过滤 getAllBaseTools():
- 简单模式 (
CLAUDE_CODE_SIMPLE) — 仅 Bash、阅读、编辑 - REPL 模式 — 隐藏原始工具; 他们住在 REPL VM 内
- 拒绝规则 — 过滤工具匹配
alwaysDenyRules在权限上下文中 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',
)
}
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)
StreamingToolExecutor 是 real-time 变体:在模型的完整响应完成之前,当工具的块从 API 流入时,它开始执行工具。 这是生产流模式中使用的类。
工具生命周期状态
每个工具都会跟踪一个 ToolStatus:
type ToolStatus = 'queued' | 'executing' | 'completed' | 'yielded'
- queued — 收到块,等待并发槽
- executing —
runToolUse()发电机正在消耗 - 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')
}
每个工具 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() 流程
- Zod验证 —
inputSchema.safeParse(input)。 失败返回一个InputValidationError工具立即得出结果。 包括未发送架构的延迟工具的提示。 - 语义验证 —
tool.validateInput()。 自定义每个工具检查(路径遍历、文件大小限制等)。 - 推测分类器 — Bash 命令推测启动允许分类器 before 挂钩运行,因此分类器结果在权限对话框可能需要时已准备就绪。
- backfillObservableInput — 创建浅克隆并添加遗留/派生字段。 克隆是 hooks 和 canUseTool 看到的; 原始(API 绑定)输入永远不会改变。
- PreToolUse 挂钩 — 异步生成器; 可以产生进度、更新输入、设置权限结果或完全停止执行。
- canUseTool() — 主要权限门。 检查始终允许/始终拒绝规则、自动分类器,并可选择显示交互式权限提示。
- tool.call() — 使用进度回调实际执行。
- PostToolUse 挂钩 — 工具完成后运行。
- 结果序列化 —
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.data 去 tool.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))
}
要点
Tool 是 TypeScript 结构类型。 任何满足该接口的对象都是一个工具——内置的、MCP 或动态生成的。 buildTool() 填写安全的失败关闭默认值,以便作者仅覆盖他们需要的内容。getAllBaseTools() → getTools() → assembleToolPool()。 在加载时对工具进行功能标记; 拒绝规则在模型看到它们之前过滤它们; isEnabled() 是最后的否决权。 MCP 工具被分类为单独的字母后缀,以保持提示缓存稳定性。isConcurrencySafe(input) 称为“按工具调用”,而不是“按工具类型”。 正在运行的 Bash 工具 ls 跑步时可能是安全的 rm -rf 不是。 编排器在运行时将工具块划分为并发/串行批次。ToolPermissionContext is DeepImmutable。 状态变化通过以下方式发生 contextModifier 返回的函数 ToolResult,在工具完成后应用。 这可以防止意外的交叉工具状态污染。siblingAbortController。 Bash 在钩子运行之前推测性地启动安全分类器。 巴什有 _simulatedSedEdit 防守被剥夺的内场。 Bash 的隐式依赖链证明将其与纯读取工具区别对待是合理的。知识检查
buildTool() 默认把 isConcurrencySafe 设成什么,为什么?backfillObservableInput() 为什么要改浅拷贝,而不是直接改 parsedInput.data?contextModifier,它会在什么时候生效?