第 3 章:Tool System — 工具调用、权限与动态披露
源码位置:
src/Tool.ts、src/services/tools/、src/tools/(56 个工具实现)
3.1 什么是工具系统?
Claude Code 的工具系统是 "行动能力" 的实现层。Claude 通过文本表达"我要执行某个操作",工具系统负责:
- 解析 Claude 的
tool_use块 - 权限检查:是否允许执行?
- 执行工具,获得结果
- 把结果返回给 Claude(以
tool_result形式)
3.2 工具定义结构
每个工具通过 buildTool() 工厂函数构建,遵循统一接口:
// src/Tool.ts
export type ToolDef<TInput, TOutput> = {
// ── 必需字段 ──────────────────────────────────────
name: string // 工具名称(如 'Bash', 'Read')
maxResultSizeChars: number // 结果最大字符数(超出则持久化到磁盘)
description(): Promise<string> // 工具描述(发送给 API 的简短说明)
prompt(): Promise<string> // 详细使用说明(注入 system prompt)
inputSchema: ZodSchema // 输入 Schema(Zod 定义,懒加载)
call(input, context): Promise<TOutput> // 执行逻辑
// ── 可选字段 ──────────────────────────────────────
searchHint?: string // ToolSearch 关键字匹配描述
strict?: boolean // 严格 JSON Schema 模式
outputSchema?: ZodSchema // 输出 Schema
isEnabled?(): boolean // 是否启用(默认 true,可按运行时条件禁用)
isReadOnly?(): boolean // 是否只读(用于 plan 模式过滤)
isDestructive?(): boolean // 是否不可逆操作(影响权限警告)
isConcurrencySafe?(): boolean // 是否可与其他工具并发(默认 false)
shouldDefer?: boolean // 是否延迟披露(Tool Search 机制)
alwaysLoad?: boolean // 即使 shouldDefer=true 也强制加载(优先级最高)
checkPermissions?(input): Promise<PermissionResult> // 权限检查(默认 allow)
interruptBehavior?(): 'cancel' | 'block'
// 用户输入新消息时的行为:
// 'cancel' — 中止当前工具执行
// 'block' — 继续执行完再处理新消息
userFacingName?(): string // 显示给用户的名称(空字符串 = 不显示)
renderToolUseMessage?(): React.ReactNode | null // UI 渲染
}
示例:TodoWriteTool 的定义
源码位置:src/tools/TodoWriteTool/TodoWriteTool.ts:31
export const TodoWriteTool = buildTool({
name: TODO_WRITE_TOOL_NAME,
searchHint: 'manage the session task checklist',
maxResultSizeChars: 100_000,
strict: true,
async description() { return DESCRIPTION },
async prompt() { return PROMPT },
get inputSchema() { return inputSchema() }, // 懒加载 Schema
get outputSchema() { return outputSchema() },
userFacingName() { return '' }, // 空字符串 = 不在 UI 显示工具名
shouldDefer: true, // 延迟披露给 Claude
isEnabled() { return !isTodoV2Enabled() }, // 仅在 v1 模式下启用
async checkPermissions(input) {
return { behavior: 'allow', updatedInput: input } // 无需权限确认
},
})
3.3 工具执行编排
源码位置:src/services/tools/toolOrchestration.ts
Claude 返回的 assistant 消息可能包含多个 tool_use 块,runTools() 负责编排它们的执行。
并发策略:按 isConcurrencySafe 分批
工具不是全部并发执行,而是根据 isConcurrencySafe() 分批处理(src/services/tools/toolOrchestration.ts:26):
假设 Claude 返回 4 个工具调用:[Read, Bash, Edit, Read]
其中 Edit.isConcurrencySafe() = false,其余 = true
分批结果:
批次 1: [Read, Bash] → 并发执行
批次 2: [Edit] → 独占执行(等待批次1全部完成)
批次 3: [Read] → 并发执行(等待批次2完成)
规则:连续的 safe 工具合并为一批并发;任何 unsafe 工具单独成一批串行执行。
并发上限(src/services/tools/toolOrchestration.ts:8):
function getMaxToolUseConcurrency(): number {
return parseInt(process.env.CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY || '', 10) || 10
}
// 默认最多 10 个工具并发,可通过环境变量调整
StreamingToolExecutor
源码位置:src/services/tools/StreamingToolExecutor.ts
处理流式工具调用(工具边接收 Claude 输出边执行)的核心类,管理并发约束队列:
// 并发执行条件检查(StreamingToolExecutor.ts)
private canExecuteTool(isConcurrencySafe: boolean): boolean {
const executingTools = this.tools.filter(t => t.status === 'executing')
return (
executingTools.length === 0 ||
(isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe))
)
}
Sibling Abort Controller:每个 StreamingToolExecutor 实例持有一个子 AbortController,当并发工具之一出错时,立即终止其他 sibling 工具(不终止整个 query turn):
// Child of toolUseContext.abortController.
// Fires when a Bash tool errors so sibling subprocesses die immediately
// instead of running to completion. Aborting this does NOT abort the parent.
private siblingAbortController: AbortController
工具执行完整流程
Claude 返回 tool_use 块
│
▼
findToolByName() ← 查找工具实现
│
▼
backfillObservableInput() ← 工具注入派生字段(不影响 call() 原始输入)
│
▼
PreToolUse hooks ← 可修改输入、提供权限决策、注入上下文
│
▼
权限检查(checkPermissions + canUseTool)
├─ allow → 继续
├─ deny → 返回错误 tool_result,跳过执行
└─ ask → 暂停等待用户确认
│
▼
call(input, toolUseContext) ← 执行工具
│
▼
applyToolResultBudget() ← 结果大小限制(见 3.6)
│
▼
PostToolUse hooks ← 可修改输出
│
▼
返回 tool_result 给 Claude
3.4 权限系统
工具执行前必须通过权限检查,这是 Claude Code 安全模型的核心。
权限检查函数
// src/hooks/useCanUseTool.tsx
export type CanUseToolFn = (
toolName: string,
input: unknown,
context: ToolPermissionContext,
) => Promise<PermissionResult>
权限结果类型
// src/utils/permissions/PermissionResult.ts
type PermissionResult =
| { behavior: 'allow'; updatedInput?: unknown } // 允许(可修改输入)
| { behavior: 'deny'; message: string } // 拒绝(附原因)
| { behavior: 'ask'; prompt: string } // 需要用户确认
权限模式
源码位置:src/types/permissions.ts
| 模式 | 说明 |
|---|---|
default | 标准权限检查,需用户确认危险操作 |
acceptEdits | 自动接受文件编辑操作 |
bypassPermissions | 绕过所有权限检查(自动化/CI 模式) |
dontAsk | 拒绝所有权限请求(只读保护模式) |
plan | 计划模式,允许读但拒绝写操作 |
auto | 自动分类器模式(需 TRANSCRIPT_CLASSIFIER feature gate) |
注意:文档旧版本列出的 bubble 模式不存在于源码中。
Hook 可以修改权限
PreToolUse Hook 能够:
- 批准或拒绝工具调用
- 修改工具输入参数
- 添加额外上下文
源码位置:src/types/hooks.ts:72
z.object({
hookEventName: z.literal('PreToolUse'),
permissionDecision: permissionBehaviorSchema().optional(), // 批准/拒绝
permissionDecisionReason: z.string().optional(),
updatedInput: z.record(z.string(), z.unknown()).optional(), // 修改输入
additionalContext: z.string().optional(), // 注入上下文
})
3.5 工具动态披露(Deferred Tool Disclosure)
这是 Claude Code 一个精妙的优化设计。并非所有工具都在每次 API 调用时都包含在 tools 参数中。
问题背景
Claude Code 有 56+ 个工具。如果每次 API 调用都传递所有工具的完整 Schema,会:
- 消耗大量 token(工具描述本身很长)
- 降低 Claude 的"专注度"(工具太多容易混淆)
解决方案:shouldDefer 标志
// 在工具定义中
shouldDefer: true // 这个工具不直接披露给 Claude
标记了 shouldDefer: true 的工具不会出现在每次 API 请求的 tools 数组中。
ToolSearch 机制
Claude 要使用延迟工具时,先调用 ToolSearch 工具搜索它:
Claude 想用某个功能(比如 TodoWrite)
│
▼
Claude 调用 ToolSearch("manage task checklist")
│
▼
ToolSearch 返回匹配的工具名和描述
│
▼
Claude 现在知道了工具名,可以调用它
// src/utils/toolSearch.ts
export function isToolSearchEnabled(): boolean { ... }
export function isDeferredToolsDeltaEnabled(): boolean { ... }
// API 响应处理:提取已发现的工具名
export function extractDiscoveredToolNames(response): string[] { ... }
extractDiscoveredToolNames:动态工具加载
ToolSearch 工具被调用后,返回的结果中包含 tool_reference blocks。extractDiscoveredToolNames() 扫描消息历史,提取所有已经被发现的工具名称(src/utils/toolSearch.ts:545):
export function extractDiscoveredToolNames(messages: Message[]): Set<string> {
const discoveredTools = new Set<string>()
for (const msg of messages) {
// compact boundary 恢复之前发现的工具
if (msg.type === 'system' && msg.subtype === 'compact_boundary') {
const carried = msg.compactMetadata?.preCompactDiscoveredTools
if (carried) for (const name of carried) discoveredTools.add(name)
}
// tool_result 中的 tool_reference blocks
for (const block of ...) {
if (isToolReferenceWithName(block)) discoveredTools.add(block.tool_name)
}
}
return discoveredTools
}
作用:只有已发现的工具才会包含在后续 API 请求的 tools 数组中。这解决了 MCP 工具数量过多的问题——100 个 MCP 工具不会全部在对话开始时就传给模型。
compact 发生时,preCompactDiscoveredTools 被写入 compact boundary message,确保压缩后已发现的工具不丢失。
附件中的延迟工具提示
// src/utils/attachments.ts
getDeferredToolsDeltaAttachment(toolUseContext)
// 返回一个消息,告诉 Claude:
// "还有以下工具可以通过 ToolSearch 发现:[工具列表摘要]"
动态披露流程图
API 请求构建阶段:
┌─────────────────────────────────────────┐
│ 全部工具列表(56+) │
│ │
│ 直接披露(tools 参数): │
│ ├─ Bash │
│ ├─ FileRead │
│ ├─ FileEdit │
│ ├─ Agent │
│ ├─ Skill │
│ ├─ ToolSearch │
│ └─ ...(未标 shouldDefer 的工具) │
│ │
│ 延迟披露(附件消息提示): │
│ ├─ TodoWrite (shouldDefer: true) │
│ ├─ TaskCreate (shouldDefer: true) │
│ ├─ EnterWorktree (shouldDefer: true) │
│ └─ ... │
└─────────────────────────────────────────┘
3.6 工具结果大小限制与持久化
工具结果可能非常大(Bash 输出、文件读取等)。Claude Code 有三层预算机制(src/utils/toolResultStorage.ts):
第一层:单工具声明上限
每个工具通过 maxResultSizeChars 声明自己的上限:
TodoWriteTool.maxResultSizeChars = 100_000ReadTool.maxResultSizeChars = Infinity(Read 工具自带 maxTokens,不走持久化)- 默认上限为 50,000 字符
GrowthBook feature flag tengu_satin_quoll 可按工具名动态覆盖阈值。最终阈值:Math.min(声明值, 50k默认值)。
第二层:单结果持久化
超过阈值的结果被写入磁盘(src/utils/toolResultStorage.ts:272),原始内容替换为引用字符串:
<persisted-output>
Output too large (26KB). Full output saved to: /path/to/session/tool-results/xxx.txt
Preview (first 2KB):
[前 2000 字节的内容预览]
</persisted-output>
Claude 看到的是这个引用,可以在需要时通过 Read 工具读取完整内容。
第三层:每条消息的聚合预算
单条消息中所有工具结果的总大小上限为 200,000 字符。多个工具并发时,超出部分按比例压缩。
3.7 工具类型分类
| 分类 | 工具名 | 说明 |
|---|---|---|
| 文件操作 | FileRead, FileEdit, FileWrite, Glob, Grep | 读写搜索文件 |
| Shell | Bash | 执行 Shell 命令 |
| 网络 | WebSearch, WebFetch | 搜索和获取网页 |
| 代理 | Agent, SendMessage | 创建子代理、发消息 |
| 任务 | TodoWrite, TaskCreate, TaskUpdate, TaskStop, TaskGet, TaskList | 任务管理 |
| 技能 | Skill, ToolSearch | 调用技能、搜索工具 |
| 团队 | TeamCreate, TeamDelete | 创建代理团队 |
| Worktree | EnterWorktree, ExitWorktree | Git Worktree 管理 |
| 规划 | EnterPlanMode, ExitPlanMode | 计划模式 |
| Cron | CronCreate, CronDelete, CronList | 定时任务 |
| 记忆 | NotebookEdit, ReadMcpResource | 笔记、MCP 资源 |
| 特殊 | Sleep, AskUserQuestion, RemoteTrigger | 休眠、提问、远程触发 |
3.8 工具上下文(ToolUseContext)
源码位置:src/Tool.ts:158
所有工具的 call(input, toolUseContext) 都接收这个上下文对象:
type ToolUseContext = {
// ── 配置 ──────────────────────────────────────
options: {
commands: Command[]
tools: Tools // 当前可用工具列表(注意:在 options 中)
mainLoopModel: string
mcpClients: MCPServerConnection[]
// ...
}
// ── 状态读写 ────────────────────────────────
getAppState(): AppState // 读取全局状态(权限上下文、任务列表等)
setAppState(f): void // 原子性更新状态(注意:不是 updateAppState)
readFileState: FileStateCache // 文件读取缓存(LRU)
// ── 取消控制 ────────────────────────────────
abortController: AbortController // 用于中止整个 query
// ── 会话标识 ────────────────────────────────
agentId?: AgentId // 子代理才有;主线程通过 getSessionId() 获取会话 ID
// 注意:没有 sessionId 字段,需调用 getSessionId() 函数
// ── Prompt Cache 优化 ───────────────────────
renderedSystemPrompt?: string // 父代理已渲染的 system prompt 字节(fork 场景)
queryTracking: { chainId, depth } // 查询链追踪(analytics)
// ── 权限 ────────────────────────────────────
// 注意:canUseTool 和 permissionMode 不在 ToolUseContext 中
// 权限模式通过 toolUseContext.getAppState().toolPermissionContext.mode 获取
// ── UI 回调(可选,仅 REPL 模式有) ──────────
setToolJSX?: SetToolJSXFn
appendSystemMessage?: (msg) => void
sendOSNotification?: (opts) => void
}
常见误区:
permissionMode不是ToolUseContext的直接字段,需通过getAppState().toolPermissionContext.mode读取canUseTool是runTools()的参数,不在ToolUseContext中sessionId字段不存在,需调用模块级getSessionId()函数
小结
工具执行完整流程:
Claude 返回 tool_use
│
▼
findToolByName() ← 查找工具实现
│
▼
isEnabled()? ← 工具是否在当前模式下启用?
│
▼
checkPermissions() ← 权限检查(可被 PreToolUse Hook 干预)
│
├─ allow → call() → 执行工具
├─ deny → 返回错误 tool_result
└─ ask → 暂停,等待用户确认
│
▼
applyToolResultBudget() ← 结果大小限制
│
▼
返回 tool_result 给 Claude
| 概念 | 源码位置 |
|---|---|
| 工具定义接口 | src/Tool.ts |
| 工具注册列表 | src/tools.ts |
| 工具执行编排(含分批并发策略) | src/services/tools/toolOrchestration.ts |
| 流式工具执行(含 sibling abort) | src/services/tools/StreamingToolExecutor.ts |
| 权限检查函数 | src/hooks/useCanUseTool.tsx |
| 权限模式定义 | src/types/permissions.ts |
| 延迟工具披露 + 动态工具加载 | src/utils/toolSearch.ts |
| 结果持久化(三层预算) | src/utils/toolResultStorage.ts |