一个 AI Agent 能做什么,本质上由它拥有哪些工具决定。工具越丰富,Agent 的能力边界越宽;工具越可靠,Agent 的行为越可预期。Claude Code 拥有 53 个工具------从读写文件、执行命令,到搜索代码、管理子代理,几乎覆盖了一个软件开发者日常工作的所有操作。
把这 53 个工具统一管理起来,并不是简单地堆放在一起。它们需要一套共同的接口规范、一套权限控制机制、一套进度反馈协议,还要能以完全相同的方式嵌入 Claude 的 system prompt 里,让模型可以一视同仁地调用任何一个。本文就带大家解析这套统一框架。
一、Tool.ts:所有工具的契约
src/Tool.ts 是整个工具系统的「宪法」,它定义了一个工具必须遵守的所有接口。
一个工具的核心结构包括以下几个字段:
typescript
type Tool = {
// 基本身份
name: string // 工具名,e.g. "Bash", "Read"
description: string // 给 Claude 看的说明,嵌入 system prompt
inputSchema: ToolInputJSONSchema // 参数的 JSON Schema,Claude 按此构造调用
// 执行
execute(
input: unknown,
context: ToolUseContext,
setToolJSX: SetToolJSXFn,
): AsyncGenerator<ToolCallProgress, ToolResultBlockParam>
// 权限检查(可选,不实现则默认允许)
canUseTool?(
input, context, assistantMessage, toolUseID, forceDecision,
): PermissionResult | Promise<PermissionResult>
// 输入验证(可选)
validateInput?(input): ValidationResult
// 工具结果大小上限(超出则存磁盘,可选)
maxResultSizeChars?: number
}
这里最值得关注的是 execute() 的返回类型------同样是一个 AsyncGenerator。工具执行过程中可以随时 yield 出 ToolCallProgress 类型的进度消息,让终端 UI 实时显示「正在执行...」「已读取 3 个文件...」这类动态状态,而不是黑屏等待。最终执行完成后,return 一个 ToolResultBlockParam,这才是真正返回给 Claude 的工具结果。
这个「进度消息 + 最终结果」的双层输出设计,是 Claude Code 体验流畅的重要原因之一。
二、工具是怎么创建的------buildTool() 工厂函数
工具不直接实现 Tool 接口,而是通过 buildTool() 工厂函数创建。这个设计的好处是:所有工具都经过同样的「质检流程」,工厂函数可以在里面注入统一的错误处理、日志记录、类型校验等横切逻辑,而不用每个工具自己实现一遍。
以 BashTool 为例,它的定义文件是 src/tools/BashTool/BashTool.tsx,导出一个 ToolDef 传给 buildTool():
typescript
buildTool({
name: BASH_TOOL_NAME, // "Bash"
description: getSimplePrompt(), // 工具说明,动态生成,注入 system prompt
inputSchema: lazySchema(z.object({
command: z.string(),
timeout: z.number().optional(),
description: z.string().optional(),
})),
execute: async function*(input, context, setToolJSX) {
// ... 执行逻辑,可以多次 yield 进度
yield { type: 'progress', message: '正在执行命令...' }
const result = await exec(input.command)
return buildToolResult(result)
},
canUseTool: bashToolHasPermission,
})
lazySchema() 包装了一下 Zod schema,做了懒加载处理------只有真正需要校验输入时才初始化 schema 对象,减少启动时的内存开销。
三、三层权限防护
使用 Claude Code 时,大家会发现有些操作自动放行,有些会弹出确认框,有些在某些模式下完全禁止。这背后是一套三层叠加的权限防护体系。
如图所示,三层权限从内到外依次拦截:
#mermaid-svg-CDOHRbsFBvYKWRnD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CDOHRbsFBvYKWRnD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CDOHRbsFBvYKWRnD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CDOHRbsFBvYKWRnD .error-icon{fill:#552222;}#mermaid-svg-CDOHRbsFBvYKWRnD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CDOHRbsFBvYKWRnD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CDOHRbsFBvYKWRnD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CDOHRbsFBvYKWRnD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CDOHRbsFBvYKWRnD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CDOHRbsFBvYKWRnD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CDOHRbsFBvYKWRnD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CDOHRbsFBvYKWRnD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CDOHRbsFBvYKWRnD .marker.cross{stroke:#333333;}#mermaid-svg-CDOHRbsFBvYKWRnD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CDOHRbsFBvYKWRnD p{margin:0;}#mermaid-svg-CDOHRbsFBvYKWRnD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CDOHRbsFBvYKWRnD .cluster-label text{fill:#333;}#mermaid-svg-CDOHRbsFBvYKWRnD .cluster-label span{color:#333;}#mermaid-svg-CDOHRbsFBvYKWRnD .cluster-label span p{background-color:transparent;}#mermaid-svg-CDOHRbsFBvYKWRnD .label text,#mermaid-svg-CDOHRbsFBvYKWRnD span{fill:#333;color:#333;}#mermaid-svg-CDOHRbsFBvYKWRnD .node rect,#mermaid-svg-CDOHRbsFBvYKWRnD .node circle,#mermaid-svg-CDOHRbsFBvYKWRnD .node ellipse,#mermaid-svg-CDOHRbsFBvYKWRnD .node polygon,#mermaid-svg-CDOHRbsFBvYKWRnD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CDOHRbsFBvYKWRnD .rough-node .label text,#mermaid-svg-CDOHRbsFBvYKWRnD .node .label text,#mermaid-svg-CDOHRbsFBvYKWRnD .image-shape .label,#mermaid-svg-CDOHRbsFBvYKWRnD .icon-shape .label{text-anchor:middle;}#mermaid-svg-CDOHRbsFBvYKWRnD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CDOHRbsFBvYKWRnD .rough-node .label,#mermaid-svg-CDOHRbsFBvYKWRnD .node .label,#mermaid-svg-CDOHRbsFBvYKWRnD .image-shape .label,#mermaid-svg-CDOHRbsFBvYKWRnD .icon-shape .label{text-align:center;}#mermaid-svg-CDOHRbsFBvYKWRnD .node.clickable{cursor:pointer;}#mermaid-svg-CDOHRbsFBvYKWRnD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CDOHRbsFBvYKWRnD .arrowheadPath{fill:#333333;}#mermaid-svg-CDOHRbsFBvYKWRnD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CDOHRbsFBvYKWRnD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CDOHRbsFBvYKWRnD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CDOHRbsFBvYKWRnD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CDOHRbsFBvYKWRnD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CDOHRbsFBvYKWRnD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CDOHRbsFBvYKWRnD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CDOHRbsFBvYKWRnD .cluster text{fill:#333;}#mermaid-svg-CDOHRbsFBvYKWRnD .cluster span{color:#333;}#mermaid-svg-CDOHRbsFBvYKWRnD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CDOHRbsFBvYKWRnD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CDOHRbsFBvYKWRnD rect.text{fill:none;stroke-width:0;}#mermaid-svg-CDOHRbsFBvYKWRnD .icon-shape,#mermaid-svg-CDOHRbsFBvYKWRnD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CDOHRbsFBvYKWRnD .icon-shape p,#mermaid-svg-CDOHRbsFBvYKWRnD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CDOHRbsFBvYKWRnD .icon-shape .label rect,#mermaid-svg-CDOHRbsFBvYKWRnD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CDOHRbsFBvYKWRnD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CDOHRbsFBvYKWRnD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CDOHRbsFBvYKWRnD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第三层:全局 PermissionMode
第二层:QueryEngine 包装层
第一层:工具级 canUseTool()
default
acceptEdits
bypassPermissions
Claude 决定调用某个工具
工具自带的权限检查
e.g. BashTool 检查命令是否在白名单里
记录所有被拒绝的调用
统计到 permission_denials 字段
当前 PermissionMode?
危险操作弹确认框
文件编辑自动允许
Bash 仍需确认
完全绕过所有检查
(仅限受信任的自动化)
第一层:工具级权限
每个工具可以自带 canUseTool() 函数。以 BashTool 为例,src/tools/BashTool/bashPermissions.ts 实现了相当细致的规则:
- 命令是否在「永久允许」规则里?(用户之前选择了「总是允许」)
- 命令是否匹配通配符规则?(
git*匹配所有 git 命令) - 命令是否包含
cd跳出项目目录?(禁止) read_only模式下,是否只有cat、ls、grep这类只读命令?
这不是简单的字符串包含检查,而是基于命令语义的分析,甚至会解析 Bash 管道操作符来判断整条复合命令的性质。
第二层:QueryEngine 包装层
在 QueryEngine.submitMessage() 里,用户传入的 canUseTool 被包了一层,专门用来追踪所有被拒绝的调用,汇总到最终的 permission_denials 字段返回给调用方,方便审计。
第三层:全局 PermissionMode
整个会话的权限模式,在启动时通过命令行参数或配置文件设定。bypassPermissions 模式是为完全受信任的自动化场景设计的------比如 CI/CD 流水线里运行的 Claude Code,不需要交互确认。
四、BashTool:最复杂工具的解剖
BashTool 光自己的目录就有 16 个文件,是 53 个工具里最复杂的,值得单独拆开来看。
安全检查(bashSecurity.ts + parseForSecurity())
在执行任何命令之前,BashTool 对命令字符串做语法树(AST)级别的解析,识别高危操作:
rm -rf加根路径/chmod 777宽泛权限修改curl | bash/wget | sh这类下载执行管道- 通过 subshell 绕过审查的技巧(
$(dangerous_cmd))
这里需要特别说明的是------这不是简单的字符串匹配,而是真正理解命令的语法结构。比如 echo "rm -rf /" 不会被拦截,因为它只是打印字符串,并不执行。
Sandbox 沙盒支持(shouldUseSandbox.ts)
Claude Code 在 macOS 上支持以沙盒方式执行命令。沙盒模式下,命令无法访问 Claude Code 工作目录之外的文件系统。就算 Claude「想」修改系统文件,macOS 沙盒机制也会拦住------这是系统级别的安全保证,不是应用层的软限制。
命令语义分类
BashTool 会为每条命令打上语义标签,供 UI 层使用:
typescript
// 搜索类 → UI 显示「Searched X files」
const BASH_SEARCH_COMMANDS = new Set(['find', 'grep', 'rg', 'ag', 'ack', ...])
// 读取类 → UI 显示「Read X files」
const BASH_READ_COMMANDS = new Set(['cat', 'head', 'tail', 'jq', 'awk', ...])
// 目录列表类 → UI 显示「Listed N directories」
const BASH_LIST_COMMANDS = new Set(['ls', 'tree', 'du'])
把「列目录」和「读文件」分开,是因为「Listed N directories」和「Read N files」在用户心理上是完全不同的操作,混淆会让大家困惑。这种对用户体验的细粒度关注,在 Claude Code 的代码里随处可见。
进度显示阈值
执行命令超过 2000ms ,才开始显示进度提示。这个 PROGRESS_THRESHOLD_MS = 2000 的设计,避免了对大量短时间命令(比如 ls、echo)显示多余的进度 UI,只在真正需要等待的场景才弹出进度条。
后台任务(KAIROS 助手模式)
在 KAIROS 助手模式下,阻塞性的 Bash 命令在主代理运行 15,000ms 后会自动切换到后台任务(LocalShellTask),让主代理不被长时间运行的命令卡住。这个常量叫 ASSISTANT_BLOCKING_BUDGET_MS = 15_000,显示出工程师对「可接受阻塞时长」是有明确定量的,不是凭感觉设的。
五、工具结果的大小管理
大家可能想过:如果工具返回了一个 10MB 的文件内容,或者 grep 返回了几千行,Claude 的 context window 会直接被撑爆。
Claude Code 通过工具结果存储与替换机制 来解决这个问题(src/utils/toolResultStorage.ts):
- 工具执行完成后,如果结果超出
maxResultSizeChars限制,完整内容写入磁盘临时文件 - 发给 Claude 的消息里,用一条「结果已截断,完整内容在
{path}」的提示替换原始内容 - 下次 query 循环的
applyToolResultBudget()会统计所有工具结果的累计大小,超出阈值的做同样处理
这个机制的关键在于------是内容替换,不是信息删除 。Claude 知道「这里有个工具结果,因为太大被存到了文件里」,它可以选择用 Read 工具去读那个文件,也可以基于已有的摘要继续工作。大家不需要担心「Claude 忘掉了执行过什么」。
六、MCP 工具的特殊地位
53 个工具里有一类特殊的------MCPTool (src/tools/MCPTool/MCPTool.tsx)。
MCPTool 不是一个具体工具,而是一个工具模板 。当 Claude Code 连接到 MCP 服务时,会枚举服务暴露的工具列表,然后为每一个 MCP 工具动态创建一个 MCPTool 实例。
从 Claude 的视角来看,MCP 工具和本地工具完全没有区别------它们都以相同的格式出现在 system prompt 里,也都以相同的格式接受调用。这种透明性就类似于苹果手机上的 App Store------不管是苹果自带的 App 还是第三方开发的 App,在 iOS 系统里都以统一的方式运行,用户不需要知道「这个 App 是谁写的」。
这种透明性是 MCP 协议设计的核心目标之一,也是为什么 Claude Code 可以通过 MCP 扩展出几乎无限的能力------只要实现了 MCP 接口,任何外部服务都可以成为 Claude 的工具。
七、工具列表是如何注入 System Prompt 的
最后介绍一个很重要的细节:这 53 个工具(加上动态创建的 MCP 工具)是如何让 Claude 「知道」它们存在的?
答案是通过 fetchSystemPromptParts() 在每次 submitMessage() 时动态构建。工具的 name、description、inputSchema 会被序列化成 Anthropic API 标准的 tools 数组,随每次请求一并发送给模型。
模型在响应里,如果决定调用工具,会返回一个 tool_use 内容块,包含工具名和参数。Claude Code 接到这个块后,在 runTools() 里根据工具名找到对应的工具对象,用 toolMatchesName() 匹配,然后调用其 execute() 方法。整个过程就像一个查字典的过程:Claude 说「我要用第 N 号工具」,代码去工具列表里查 N 号工具是什么,然后执行。
学习完本篇内容,大家应该清楚了:
Tool.ts定义了统一契约,buildTool()工厂函数保证所有工具经过同样的质检execute()返回AsyncGenerator,支持在执行过程中流式输出进度消息- 三层权限防护(工具级 → 记录层 → 全局模式)在安全性和便利性之间取得平衡
- BashTool 是最复杂的工具:AST 安全检查、沙盒、命令语义分类、进度阈值,每一处都有具体的工程权衡
- 工具结果大小管理是「替换内容,保留信息」,不让大文件撑爆 context
- MCPTool 是动态适配器,让外部服务对 Claude 完全透明
接下来,进入第四篇------上下文管理,当 context window 快满的时候,Claude Code 是怎么应对的。