Claude Code Tool System 架构分析
1. 系统概览
Claude Code 的 Tool 系统是整个 CLI 的核心执行引擎。每个工具都是一个实现了统一接口的 TypeScript 对象,通过 buildTool() 工厂函数构造,支持权限校验、输入验证、并发控制、UI 渲染和结果映射。工具系统约 43 个工具目录,覆盖文件操作、Shell 执行、Web 搜索、子 Agent 调度、MCP 协议集成等场景。
关键设计原则:
- 工厂模式统一接口: 所有工具通过
buildTool()构建,自动填充安全默认值 - Fail-closed 权限模型:
isReadOnly/isConcurrencySafe默认为false,未实现时假设不可读且不可并发 - Zod schema 驱动: 输入/输出使用
zod/v4做运行时类型校验 - 懒加载 schema: 使用
lazySchema()避免循环依赖和启动开销 - 条件编译: 使用
feature()做 dead code elimination,按环境动态启用/禁用工具
2. 核心类型定义
2.1 Tool 类型 (src/Tool.ts:362-695)
Tool 是整个系统的核心泛型类型:
// src/Tool.ts:362-366
export type Tool<
Input extends AnyObject = AnyObject,
Output = unknown,
P extends ToolProgressData = ToolProgressData,
> = {
readonly name: string
aliases?: string[] // 重命名兼容
searchHint?: string // ToolSearch 关键词匹配
// --- 核心方法 ---
call(args, context, canUseTool, parentMessage, onProgress): Promise<ToolResult<Output>>
description(input, options): Promise<string>
prompt(options): Promise<string>
// --- Schema ---
readonly inputSchema: Input // Zod schema
readonly inputJSONSchema?: ToolInputJSONSchema // MCP JSON Schema
outputSchema?: z.ZodType<unknown>
// --- 能力标志 ---
isConcurrencySafe(input): boolean // 默认 false
isEnabled(): boolean // 默认 true
isReadOnly(input): boolean // 默认 false
isDestructive?(input): boolean // 不可逆操作标记
// --- 权限 ---
validateInput?(input, context): Promise<ValidationResult>
checkPermissions(input, context): Promise<PermissionResult>
// --- UI 渲染 ---
renderToolUseMessage(input, options): React.ReactNode
renderToolResultMessage(content, progressMessages, options): React.ReactNode
renderToolUseProgressMessage?(progressMessages, options): React.ReactNode
renderToolUseErrorMessage?(result, options): React.ReactNode
renderToolUseRejectedMessage?(input, options): React.ReactNode
// --- 结果映射 ---
mapToolResultToToolResultBlockParam(content, toolUseID): ToolResultBlockParam
extractSearchText?(out: Output): string
}
2.2 ToolDef 类型 (src/Tool.ts:721-726)
ToolDef 是 Tool 的部分定义,可省略 buildTool 会自动填充的方法:
// src/Tool.ts:721-726
type DefaultableToolKeys =
| 'isEnabled' | 'isConcurrencySafe' | 'isReadOnly'
| 'isDestructive' | 'checkPermissions' | 'toAutoClassifierInput'
| 'userFacingName'
export type ToolDef<Input, Output, P> =
Omit<Tool<Input, Output, P>, DefaultableToolKeys> &
Partial<Pick<Tool<Input, Output, P>, DefaultableToolKeys>>
2.3 buildTool() 工厂函数 (src/Tool.ts:783-792)
// src/Tool.ts:757-769 — 安全默认值
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: (_input?: unknown) => false, // 默认非并发安全
isReadOnly: (_input?: unknown) => false, // 默认假设写入
isDestructive: (_input?: unknown) => false,
checkPermissions: (input, _ctx) =>
Promise.resolve({ behavior: 'allow', updatedInput: input }),
toAutoClassifierInput: (_input?: unknown) => '',
userFacingName: (_input?: unknown) => '',
}
// src/Tool.ts:783-792
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
return {
...TOOL_DEFAULTS,
userFacingName: () => def.name,
...def,
} as BuiltTool<D>
}
2.4 ToolResult<T> 类型 (src/Tool.ts:321-336)
export type ToolResult<T> = {
data: T
newMessages?: (UserMessage | AssistantMessage | AttachmentMessage | SystemMessage)[]
contextModifier?: (context: ToolUseContext) => ToolUseContext
mcpMeta?: { _meta?: Record<string, unknown>; structuredContent?: Record<string, unknown> }
}
3. 工具注册与调度机制
3.1 工具注册 (src/tools.ts:193-251)
所有工具通过 getAllBaseTools() 函数注册。这是一个静态函数(非运行时注册表),直接返回工具数组:
// src/tools.ts:193-251
export function getAllBaseTools(): Tools {
return [
AgentTool,
TaskOutputTool,
BashTool,
...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]), // 条件注册
ExitPlanModeV2Tool,
FileReadTool, FileEditTool, FileWriteTool,
NotebookEditTool, WebFetchTool, TodoWriteTool,
WebSearchTool, TaskStopTool, AskUserQuestionTool,
SkillTool, EnterPlanModeTool,
// ... 更多条件工具
...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []),
]
}
3.2 工具过滤链 (src/tools.ts:271-327)
getTools() 实现了多层过滤:
- 简单模式过滤:
CLAUDE_CODE_SIMPLE环境变量时只保留 Bash/Read/Edit - Deny 规则过滤:
filterToolsByDenyRules()移除被权限策略禁用的工具 - REPL 模式过滤: REPL 启用时隐藏内部原始工具(它们通过 REPL VM 间接可访问)
- isEnabled 过滤: 调用每个工具的
isEnabled()方法
// src/tools.ts:271-327
export const getTools = (permissionContext: ToolPermissionContext): Tools => {
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) { /* 只返回 Bash/Read/Edit */ }
const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name))
let allowedTools = filterToolsByDenyRules(tools, permissionContext)
if (isReplModeEnabled()) { /* 过滤 REPL_ONLY_TOOLS */ }
const isEnabled = allowedTools.map(_ => _.isEnabled())
return allowedTools.filter((_, i) => isEnabled[i])
}
3.3 工具池组装 (src/tools.ts:345-367)
assembleToolPool() 合并内置工具和 MCP 工具:
export function assembleToolPool(permissionContext, mcpTools): Tools {
const builtInTools = getTools(permissionContext)
const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
// 内置工具在前(优先级更高),MCP 工具在后,各自按名称排序保证缓存稳定性
const byName = (a, b) => a.name.localeCompare(b.name)
return uniqBy(
[...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
'name',
)
}
4. 工具执行生命周期
一个工具调用的完整流程:
模型输出 tool_use 块
│
▼
1. findToolByName() — 按 name 或 aliases 查找工具
│
▼
2. backfillObservableInput() — 补充/规范化输入(如展开 ~ 路径)
│
▼
3. validateInput() — 纯逻辑校验(无 I/O 或轻量 I/O)
│ ├─ 返回 { result: false, message, errorCode } → 工具执行中止,告知模型原因
│ └─ 返回 { result: true } → 继续
│
▼
4. checkPermissions() — 权限检查(工具特定逻辑 + 通用权限系统)
│ ├─ 'deny' → 拒绝执行,通知模型
│ ├─ 'ask' → 弹出用户确认对话框
│ └─ 'allow' → 继续
│
▼
5. call() — 实际执行
│ ├─ 返回 ToolResult<Output>
│ ├─ 可通过 onProgress 回调报告进度
│ └─ 可通过 newMessages 附加额外消息
│
▼
6. mapToolResultToToolResultBlockParam() — 将输出转换为 API 格式
│
▼
7. renderToolResultMessage() — 渲染 UI 结果
5. 权限模型
5.1 权限类型层次 (src/types/permissions.ts)
// 权限行为:三方决策
type PermissionBehavior = 'allow' | 'deny' | 'ask'
// 权限模式:控制整体策略
type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions'
| 'dontAsk' | 'plan' | 'auto' | 'bubble'
// 权限规则
type PermissionRule = {
source: PermissionRuleSource // userSettings, projectSettings, localSettings, ...
ruleBehavior: PermissionBehavior
ruleValue: { toolName: string; ruleContent?: string }
}
5.2 两层权限检查
第一层:工具内 validateInput() (src/Tool.ts:489-492)
纯逻辑校验,不依赖通用权限系统。返回 ValidationResult:
type ValidationResult =
| { result: true }
| { result: false; message: string; errorCode: number }
示例 — FileReadTool.validateInput() (src/tools/FileReadTool/FileReadTool.ts:418-495):
- 检查 PDF 页码范围格式
- 检查 deny 规则
- 检查 UNC 路径安全
- 检查二进制文件扩展名
- 检查阻塞设备文件路径
第二层:checkPermissions() (src/Tool.ts:500-503)
工具特定权限 + 通用权限系统的联合检查:
// src/Tool.ts:500-503
checkPermissions(input: z.infer<Input>, context: ToolUseContext): Promise<PermissionResult>
PermissionResult 是一个 union type (src/types/permissions.ts:174-230+):
PermissionAllowDecision:{ behavior: 'allow', updatedInput?, decisionReason? }PermissionDenyDecision:{ behavior: 'deny', message, decisionReason }PermissionAskDecision:{ behavior: 'ask', message, suggestions?, decisionReason? }
5.3 权限检查策略 (工具特定 vs 通用)
不同工具实现不同的权限策略:
| 工具 | checkPermissions 策略 |
|---|---|
| BashTool | bashToolHasPermission() — 完整的命令语义分析 (bashPermissions.ts, 2621行) |
| FileReadTool | checkReadPermissionForTool() — 文件系统读权限 |
| FileEditTool/FileWriteTool | 写权限检查 + settings 文件特殊验证 |
| WebFetchTool | 域名级别 allow/deny/ask 规则 + 预批准主机列表 |
| GrepTool/GlobTool | checkReadPermissionForTool() — 读权限(与 FileReadTool 共享) |
| MCPTool | 返回 behavior: 'passthrough' — 委托给 MCP 客户端 |
5.4 BashTool 权限模型详解 (src/tools/BashTool/bashPermissions.ts)
BashTool 拥有最复杂的权限系统(2621行):
- 命令 AST 解析: 使用
parseForSecurity()解析 shell 命令的 AST - 子命令级权限: 复合命令中每个子命令独立检查(
ls && git push中git push需要权限) - 安全分类器:
classifyBashCommand()进行风险分级 - sed 模拟编辑: 检测到 sed 原地编辑时,转换为模拟编辑以便用户预览 diff
- 沙箱支持:
shouldUseSandbox()决定是否在沙箱中运行
6. 详细工具实现分析
6.1 BashTool (src/tools/BashTool/)
文件结构 (18 个文件):
BashTool.tsx— 主入口 (1144 行)bashPermissions.ts— 权限检查 (2621 行)bashSecurity.ts— 安全分析sedEditParser.ts— sed 命令解析,将 sed 转换为 FileEdit 操作readOnlyValidation.ts— 只读约束检查commandSemantics.ts— 命令结果语义解释shouldUseSandbox.ts— 沙箱决策prompt.ts— 系统提示词 (369 行)
输入 Schema:
z.strictObject({
command: z.string(),
description: z.string().optional(),
timeout: z.number().optional(),
run_in_background: z.boolean().optional(),
})
关键特性:
isReadOnly动态判断:分析命令是否为只读操作 (line 437-441)isConcurrencySafe基于isReadOnly(line 434-436)- 自动后台化:超时命令自动转后台执行
- sed 拦截:检测 sed 原地编辑,直接操作文件以支持用户预览 diff
- 进度报告:通过 async generator
runShellCommand()实时输出
6.2 FileReadTool (src/tools/FileReadTool/)
文件结构 (5 个文件):
FileReadTool.ts— 主入口 (1183 行)imageProcessor.ts— 图片处理limits.ts— 读取限制prompt.ts— 提示词UI.tsx— UI 渲染
输入 Schema:
z.strictObject({
file_path: z.string(),
offset: z.number().int().nonnegative().optional(),
limit: z.number().int().positive().optional(),
pages: z.string().optional(), // PDF 页码范围
})
输出 Schema: 判别联合类型 (discriminatedUnion),支持 6 种输出:
text— 普通文本文件image— 图片(base64 编码)notebook— Jupyter notebookpdf— PDF 文件(base64)parts— 提取的 PDF 页面图片file_unchanged— 去重优化(重复读取同一文件返回 stub)
关键设计:
- 文件去重:
readFileState缓存文件状态(mtime + content),相同范围的重复读取返回file_unchangedstub (line 540-573) - Token 预算:
validateContentTokens()确保内容不超过 token 限制 - 图片压缩: 基于 token 预算的多级压缩策略
- maxResultSizeChars = Infinity: 结果永不被持久化到磁盘(避免 Read→file→Read 循环)
- CYBER_RISK_MITIGATION_REMINDER: 读取文件时注入恶意代码分析提醒
6.3 AgentTool (src/tools/AgentTool/)
文件结构 (15 个文件):
AgentTool.tsx— 主入口 (1398 行)runAgent.ts— Agent 执行引擎forkSubagent.ts— Fork 子 AgentloadAgentsDir.ts— Agent 定义加载resumeAgent.ts— Agent 恢复built-in/— 内置 Agent 定义
输入 Schema: 基础 + 扩展
// 基础
z.object({
description: z.string(),
prompt: z.string(),
subagent_type: z.string().optional(),
model: z.enum(['sonnet', 'opus', 'haiku']).optional(),
run_in_background: z.boolean().optional(),
})
// 扩展(feature-gated)
+ { name, team_name, mode, isolation, cwd }
输出 Schema: 两种模式
- 同步:
{ status: 'completed', result, prompt, ... } - 异步:
{ status: 'async_launched', agentId, description, prompt }
关键设计:
- 子 Agent 调度: 通过
runAgent()启动独立的 Agent 执行上下文 - 后台 Agent:
run_in_background支持异步 Agent,返回async_launched状态 - 工作区隔离:
isolation: 'worktree'创建临时 git worktree - 工具池过滤: 通过
ASYNC_AGENT_ALLOWED_TOOLS常量限制子 Agent 可用工具 (src/constants/tools.ts:55-71) - Fork 子 Agent:
forkSubagent.ts实现高效的子 Agent 分叉
6.4 MCPTool (src/tools/MCPTool/)
文件结构 (4 个文件):
MCPTool.ts— 主入口 (77 行) — 非常精简classifyForCollapse.ts— UI 折叠分类prompt.ts— 提示词UI.tsx— UI 渲染
核心特点: 这是一个模板/原型工具,实际的 MCP 工具在 mcpClient.ts 运行时动态克隆和填充:
// src/tools/MCPTool/MCPTool.ts:27-77
export const MCPTool = buildTool({
isMcp: true, // MCP 标记
name: 'mcp', // 占位名 — 被 mcpClient.ts 覆盖
maxResultSizeChars: 100_000,
// 以下方法全部被覆盖
async call() { return { data: '' } },
async checkPermissions() { return { behavior: 'passthrough', ... } },
inputSchema: z.object({}).passthrough(), // 接受任意输入
})
MCP 工具特殊性:
isOpenWorld(): 返回 false — MCP 工具作用域不透明isMcp = true: 全局标记,用于工具池过滤和权限检查- 权限返回
passthrough:委托给 MCP 客户端层面处理 inputSchema使用.passthrough()允许任意额外字段
6.5 GrepTool (src/tools/GrepTool/GrepTool.ts)
简洁实现 (577 行):基于 ripgrep 的文件内容搜索工具。
输入 Schema:
z.strictObject({
pattern: z.string(),
path: z.string().optional(),
glob: z.string().optional(),
output_mode: z.enum(['content', 'files_with_matches', 'count']).optional(),
'-B', '-A', '-C', context: z.number().optional(), // 上下文行
'-n': z.boolean().optional(), // 行号
'-i': z.boolean().optional(), // 大小写不敏感
type: z.string().optional(), // 文件类型
head_limit: z.number().optional(),
offset: z.number().optional(),
multiline: z.boolean().optional(),
})
关键设计:
isConcurrencySafe = true,isReadOnly = true— 纯读取操作isSearchOrReadCommand = { isSearch: true }— UI 可折叠maxResultSizeChars = 20_000— 超过 20K 字符持久化到磁盘- 排除 VCS 目录(
.git,.svn,.hg等) - 结果按 mtime 排序(最近修改的文件优先)
applyHeadLimit()实现分页,支持 offset
6.6 WebFetchTool (src/tools/WebFetchTool/WebFetchTool.ts)
URL 内容获取 (318 行)。
权限模型特别之处:
- 预批准主机:
isPreapprovedHost()跳过权限检查 - 域名级规则: 从 URL 提取 hostname,匹配
domain:hostname形式的权限规则 - 默认 ask: 没有匹配规则时,请求用户确认
// src/tools/WebFetchTool/WebFetchTool.ts:104-180
async checkPermissions(input, context): Promise<PermissionDecision> {
// 1. 预批准主机 → allow
if (isPreapprovedHost(hostname, pathname)) return { behavior: 'allow', ... }
// 2. deny 规则 → deny
// 3. ask 规则 → ask
// 4. allow 规则 → allow
// 5. 无规则 → ask (默认)
return { behavior: 'ask', suggestions: buildSuggestions(ruleContent) }
}
7. 跨工具通用模式
7.1 Schema 定义模式
所有工具使用 lazySchema() 包装 Zod schema,避免循环依赖:
const inputSchema = lazySchema(() => z.strictObject({ ... }))
type InputSchema = ReturnType<typeof inputSchema>
// 在 ToolDef 中:
get inputSchema(): InputSchema { return inputSchema() }
7.2 工具目录结构
每个工具遵循标准目录结构:
src/tools/ToolName/
├── ToolName.ts(x) — 主入口(buildTool 调用)
├── prompt.ts — 提示词和常量
├── UI.tsx — React UI 渲染组件
├── [utils.ts] — 可选工具函数
├── [limits.ts] — 可选限制配置
└── [permissions.ts] — 可选权限逻辑
7.3 UI 渲染方法
每个工具需要实现多个 UI 方法:
renderToolUseMessage()— 工具调用时的输入展示renderToolResultMessage()— 执行完成后的结果展示renderToolUseProgressMessage?()— 执行中的进度展示renderToolUseErrorMessage?()— 错误时的展示renderToolUseRejectedMessage?()— 权限拒绝时的展示
这些方法返回 React.ReactNode,由 Ink 在终端中渲染。
7.4 并发安全模式
// 只读工具声明并发安全
isConcurrencySafe() { return true }
isReadOnly() { return true }
// 写入工具默认非并发安全(TOOL_DEFAULTS 中 isConcurrencySafe = false)
// BashTool 动态判断
isConcurrencySafe(input) { return this.isReadOnly(input) }
并发安全的工具可以并行执行,非并发安全的工具会被串行化。
7.5 工具结果大小管理
每个工具声明 maxResultSizeChars,超过阈值的结果会自动持久化到磁盘:
| 工具 | maxResultSizeChars | 说明 |
|---|---|---|
| FileReadTool | Infinity | 永不持久化(避免循环) |
| BashTool | 30,000 | |
| GrepTool | 20,000 | |
| MCPTool | 100,000 | |
| WebFetchTool | 100,000 |
7.6 toAutoClassifierInput() 模式
用于安全分类器的输入摘要。BashTool 返回整个命令字符串,FileReadTool 返回文件路径,空字符串表示跳过分类器:
// BashTool
toAutoClassifierInput(input) { return input.command }
// FileReadTool
toAutoClassifierInput(input) { return input.file_path }
// 无安全相关性的工具可省略(默认 '')
7.7 preparePermissionMatcher() 模式
为 Hook 的 if 条件准备匹配器。BashTool 分析命令 AST 提取子命令,FileReadTool 匹配文件路径:
// BashTool (src/tools/BashTool/BashTool.tsx:445-467)
async preparePermissionMatcher({ command }) {
const parsed = await parseForSecurity(command)
if (parsed.kind !== 'simple') return () => true // fail-safe
const subcommands = parsed.commands.map(c => c.argv.join(' '))
return pattern => { /* 匹配逻辑 */ }
}
8. Notable Design Decisions
8.1 buildTool 工厂 vs 手写完整 Tool
所有工具使用 buildTool({ ...TOOL_DEFAULTS, ...def }) 模式,而不是直接导出 Tool 对象。好处:
- 安全默认值:
isReadOnly/isConcurrencySafe默认 false(fail-closed) - 类型推断:
BuiltTool<D>在类型级别模拟运行时 spread,保持精确类型 - 零运行时开销: 只做一次对象展开
8.2 MCPTool 作为原型模式
MCPTool 是一个"空壳"工具,mcpClient.ts 在运行时克隆它并填充真实的 name、schema、call 实现。这是一种 prototype 模式,避免为每个 MCP 服务器创建独立的工具类。
8.3 权限与验证的分离
validateInput() 和 checkPermissions() 是两个独立阶段:
validateInput: 纯逻辑校验(格式正确性、路径存在性)—— 失败时直接返回错误给模型checkPermissions: 权限决策(用户是否允许)—— 可弹出 UI 确认
8.4 条件编译与 Dead Code Elimination
通过 feature() 和 process.env 实现编译时工具过滤:
// src/tools.ts:16-18
const REPLTool = process.env.USER_TYPE === 'ant'
? require('./tools/REPLTool/REPLTool.js').REPLTool
: null
这确保未启用的功能不会出现在打包产物中。
8.5 工具池分层过滤
getAllBaseTools() — 全量工具列表
↓ filterToolsByDenyRules() — 按权限策略过滤
↓ REPL 模式过滤 — 隐藏 REPL 内部工具
↓ isEnabled() 过滤 — 功能开关过滤
↓ + MCP 工具 — 合并外部工具
↓ uniqBy('name') — 去重(内置优先)
= 最终工具池
8.6 子 Agent 工具限制
异步子 Agent 的工具集被严格限制(ASYNC_AGENT_ALLOWED_TOOLS,src/constants/tools.ts:55-71):
- 只允许文件读写、搜索、Shell 等基础工具
- 不允许 AgentTool(防止无限递归)
- 不允许 AskUserQuestionTool(无交互环境)
- 不允许 TaskStopTool 等管理工具
文件索引
| 文件 | 行数 | 职责 |
|---|---|---|
| src/Tool.ts | 792 | 核心类型定义、buildTool 工厂 |
| src/tools.ts | 389 | 工具注册、过滤、组装 |
| src/constants/tools.ts | 112 | 工具集合常量(子 Agent 限制等) |
| src/types/permissions.ts | 441 | 权限类型定义 |
| src/tools/BashTool/BashTool.tsx | 1144 | Bash 工具主实现 |
| src/tools/BashTool/bashPermissions.ts | 2621 | Bash 权限系统 |
| src/tools/FileReadTool/FileReadTool.ts | 1183 | 文件读取工具 |
| src/tools/GrepTool/GrepTool.ts | 577 | 正则搜索工具 |
| src/tools/WebFetchTool/WebFetchTool.ts | 318 | URL 内容获取 |
| src/tools/MCPTool/MCPTool.ts | 77 | MCP 工具原型 |
| src/tools/AgentTool/AgentTool.tsx | 1398 | 子 Agent 调度 |