Claude Code 源码解读:工具系统全解析
Claude Code 的工具系统是其核心能力的载体,40+ 内置工具通过五层架构实现从注册到执行的完整链路。
更新于 2026.08.07
本文目录 12 节

深入 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。这保证了:
- 运行时类型安全:
tool.inputSchema.safeParse(input)在执行前校验模型输出 - API 兼容性:JSON Schema 格式符合 Anthropic API 的 tool 定义规范
- 缓存友好: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 命令 |
auto | AI 分类器自动审批安全操作 | 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),在操作系统层面限制工具的权限:
| 平台 | 沙箱实现 |
|---|---|
| macOS | sandbox-exec |
| Linux | bubblewrap |
| 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 | 说明 |
|---|---|---|
| Glob | 100,000 | 文件列表可能很长 |
| Bash | 默认值(50K) | 可通过常量调整 |
| Read | Infinity | 自限制(max 25K tokens),避免循环读取 |
| Grep | 默认值(50K) | 搜索结果 |
| 全局默认 | 50,000 | DEFAULT_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— 错误格式化
评论
无需登录,审核通过后公开。只有博主可以回复。
正在加载…
已公开的评论