如果说 agent loop 是 Claude Code 的发动机,那 tool system 就是它的传动系统。
因为模型是否能真正完成任务,最终取决于它能不能发现合适的工具、理解工具的输入结构、通过权限检查、在 runtime 中正确执行,并把结果重新回填到消息链。
Claude Code 的工具系统覆盖的是一整套运行机制,不只是“模型能调用几个函数”这么简单。
系列回顾:
- Claude Code 源码深度解析之架构总览:它如何成为一套 agent runtime
- Claude Code 源码深度解析之 Harness Engineering 上篇:如何从用户输入进入运行时主循环
- Claude Code 源码深度解析之 Harness Engineering 下篇:如何治理工具、权限、恢复与长会话
- Claude Code 源码深度解析之 Context Engineering:如何管理 prompt 之外的“上下文工程”
- Claude Code 源码深度解析之 Prompt / Context 结构:Claude Code 到底把哪些东西送进模型
- Claude Code 源码深度解析之 Skill 系统:一套高层工作流框架
Tool 在 Claude Code 里到底是什么
在 src/Tool.ts 里,一个 Tool 至少包含 name、inputSchema、description()、call()、prompt()、isEnabled()、isReadOnly()、isConcurrencySafe()、checkPermissions()、interruptBehavior(),以及各种 UI、render、summary、progress 相关能力。
可以把 Tool 看成一个完整的运行时契约,不只是普通函数对象。来看实际的类型定义(节选):
// src/Tool.ts
exporttype Tool<Input, Output, P> = {
readonly name: string
readonly inputSchema: Input
call(args, context: ToolUseContext, canUseTool, parentMessage, onProgress?): Promise<ToolResult<Output>>
description(input, options): Promise<string>
prompt(input, context): Promise<string>
isEnabled(): boolean
isReadOnly(input): boolean
isConcurrencySafe(input): boolean
checkPermissions(input, context): Promise<ToolPermissionCheck>
interruptBehavior?(): 'cancel' | 'block'
// ... 还有 20+ 个 UI、渲染、transcript 相关字段
}
注意 call() 需要接收完整的 ToolUseContext,checkPermissions() 会在执行前被调用,isConcurrencySafe() 决定是否可以和其他工具并行执行。
一图看懂 Tool 抽象

Claude Code 的工具系统之所以重要,正因为它同时服务了模型、runtime、权限层和 UI 层。
tools.ts:系统可能拥有哪些工具
src/tools.ts 可以理解为 Claude Code 的能力总表。
在 getAllBaseTools() 里,可以看到它聚合了大量工具:
// src/tools.ts
exportfunctiongetAllBaseTools(): Tools{
return [
AgentTool,
TaskOutputTool,
BashTool,
...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
FileReadTool, FileEditTool, FileWriteTool,
NotebookEditTool,
WebFetchTool, TodoWriteTool, WebSearchTool,
TaskStopTool, AskUserQuestionTool, SkillTool,
...(process.env.USER_TYPE === 'ant' ? [ConfigTool] : []),
...(WebBrowserTool ? [WebBrowserTool] : []),
// ... 更多条件加载工具
]
}
这里面既有 AgentTool、BashTool、FileRead / FileEdit / FileWrite、Glob / Grep、WebFetch / WebSearch、TodoWrite、SkillTool,也包括 MCP 资源相关工具,以及 NotebookEditTool、TaskStopTool、AskUserQuestionTool、SendMessageTool 等。除此之外,还有不少按条件加载的工具,例如 ConfigTool、WebBrowserTool、PowerShellTool、ToolSearchTool,实际集合会随着 feature gate 和运行环境变化。
但要注意:出现在 tools.ts 中,不等于一定会被模型看见并使用。
因为后面还会经过环境 gating、feature gating、权限过滤、defer loading / ToolSearch,以及 MCP 动态状态等一系列处理。
工具不是全部一次性硬暴露给模型
Claude Code 在把工具暴露给模型之前,会先做一轮筛选,而不是把全部工具原样交出去。

这个设计很关键,因为它说明 Claude Code 面向模型暴露的是一个 动态工具面,而不是一组静态函数库。
如果按当前源码再往前看一步,这个“动态工具面”已经不仅仅是 gate/filter 的问题了。src/services/api/claude.ts 里现在还有一层更激进的做法:当 ToolSearch 启用时,一部分工具会先以 defer_loading 形式延后,不在第一轮就全部发给模型;模型先通过 ToolSearchTool 发现相关工具,只有真正被发现过的 deferred tools 才会进入后续请求的 filteredTools。这等于把“工具暴露”从静态列举,推进到了discovery-driven capability surfacing。
工具定义是如何变成模型可见能力的
看到 src/Tool.ts 之后,一个很自然的问题是:既然 Tool 已经定义好了,模型是不是直接就能“看到”这些对象?实际上不是。
Claude Code 中的 Tool 定义,首先是运行时对象,后面才会被转换成模型 API 能理解的工具声明。中间大致会经过这样一条链路:

关键点在于:模型并不会直接接触 call() 这种运行时代码,它看到的是一个声明式工具 schema。
在 src/services/api/claude.ts 里,这个转换过程很明确:系统先拿到 filteredTools,再把每个 Tool 交给 toolToAPISchema(...),生成最终发给模型的 toolSchemas。
// src/services/api/claude.ts
const toolSchemas = awaitPromise.all(
filteredTools.map(tool =>
toolToAPISchema(tool, {
getToolPermissionContext: options.getToolPermissionContext,
tools,
agents: options.agents,
allowedAgentTypes: options.allowedAgentTypes,
model: options.model,
deferLoading: willDefer(tool),
}),
),
)
也就是说,真正发给模型的不是原始 Tool 对象,而是从 Tool 中抽取出来的一组接口信息:
工具名,例如 name输入结构,例如 inputSchema面向模型的能力说明,例如 description()生成的描述某些与 defer loading、agent、MCP 状态有关的附加提示
从架构上看,这一步把 Tool 拆成了两个面向不同对象的层:
面向 runtime 的执行层: call()、权限检查、并发属性、UI 元数据面向模型的声明层: name、description、input schema
所以更准确地说,Claude Code 不是把“工具实现”发给模型,而是把“工具接口说明”发给模型。
而且这一步也越来越像 API 层的工程优化点。当前 toolToAPISchema() 不只是在做普通序列化,它还会按模型能力和 provider 条件决定要不要开启 strict schema、eager_input_streaming 这类附加字段。前者让一部分工具可以走更严格的结构化输出约束,后者则是在支持的直连 API 场景下启用更细粒度的 tool input streaming,避免大输入工具在参数流式阶段长时间卡住。换句话说,Claude Code 现在不仅在“定义工具”,还在按不同模型接口特性编排工具声明的发送方式。
如果只抓住最关键的组织关系,可以把它概括成下面这样的简化结构:
{
system: "你是 Claude Code ...",
messages: [
{ role: 'user', content: '帮我搜索 assembleToolPool 在哪里定义' },
{ role: 'assistant', content: '...' },
],
tools: [
{
name: 'Grep',
description: 'Search file contents with a regular expression',
input_schema: {
type: 'object',
properties: {
pattern: { type: 'string' },
path: { type: 'string' },
},
},
},
{
name: 'Read',
description: 'Read a file from the local filesystem',
input_schema: {
type: 'object',
properties: {
file_path: { type: 'string' },
},
},
},
],
}
这里的重点不是字段名一定逐字如此,而是它反映了一个稳定事实:工具定义并不混在消息正文里,而是作为与 messages 并列的一块能力上下文,单独放进模型请求中。
这样一来,模型在生成下一步时,不只是读到对话历史,也同时看到了当前可调用的能力面。它随后输出的 tool_use,本质上就是在这组工具声明里选一个名字,并按对应 input_schema 组织参数。
运行时权限链:真正的控制权不在模型手里
Claude Code 对工具最关键的约束之一,是权限系统。
在 src/hooks/useCanUseTool.tsx 里,你能看到典型的权限调用流程。它会调用 hasPermissionsToUseTool(...)(定义在 src/utils/permissions/permissions.ts 中),得到 allow / deny / ask;allow 就直接放行,deny 就拒绝并记录,ask 则进入交互式确认或其他宿主路径。
权限函数的实际签名:
// src/utils/permissions/permissions.ts
exportconst hasPermissionsToUseTool: CanUseToolFn = async (
tool, input, context, assistantMessage, toolUseID,
): Promise<PermissionDecision> => {
const result = await hasPermissionsToUseToolInner(tool, input, context)
if (result.behavior === 'allow') {
// 重置连续拒绝计数
}
return result
}
而在 useCanUseTool.tsx 里,分支处理很清晰:
// src/hooks/useCanUseTool.tsx
if (result.behavior === 'allow') {
ctx.logDecision({ decision: 'accept', source: 'config' })
resolve(ctx.buildAllow(result.updatedInput ?? input, { ... }))
}
// ...
case'deny':
logPermissionDecision({ ... }, { decision: 'reject', source: 'config' })
resolve(result)
case'ask':
// 进入交互式确认或 coordinator 路径

这条链非常值得强调:
模型只能提议调用工具,真正的放行权掌握在 runtime 手里。
来看权限系统的核心数据模型:
// src/types/permissions.ts
exporttype PermissionRule = {
source: 'userSettings' | 'projectSettings' | 'localSettings'
| 'flagSettings' | 'policySettings' | 'cliArg'
| 'command' | 'session'
ruleBehavior: PermissionBehavior // allow / deny / ask
ruleValue: {
toolName: string
ruleContent?: string// 如“只允许读 src/ 下的文件”
}
}
exporttype PermissionDecision<Input> =
| { behavior: 'allow', updatedInput?, decisionReason? }
| { behavior: 'ask', message, suggestions? }
| { behavior: 'deny', message, decisionReason }
注意 PermissionRule 的 source 字段:权限规则可以来自用户设置、项目设置、本地设置、CLI 参数、命令级或会话级。多层规则会按优先级合并,最终产出一个明确的 allow / deny / ask 决策。
工具执行是一条编排链路
在 toolOrchestration.ts 里,Claude Code 会先识别哪些工具是并发安全的,再并发执行 read-only / concurrency-safe 批次,同时把非并发工具留在串行路径上。等工具执行结束后,系统还会接收它们返回的 contextModifier,并更新当前 ToolUseContext。
这意味着工具执行会反过来影响后续 loop 状态。
tool_use -> tool_result 才是核心闭环

这张图概括了 Claude Code 工具系统最核心的闭环:Tool 不只是可调用动作,它的结果还必须重新进入消息链;而消息链更新之后,下一轮模型推理才会继续。
所以工具系统不在外围,它处在 agent loop 的中轴位置。
为什么 Tool 会有那么多元数据
因为 Claude Code 在工具层面要解决的远不只是“能不能调”。它还要回答:模型如何理解这个工具,工具是否只读、是否危险、能否并发,被中断时如何处理,结果如何展示,以及 transcript 如何索引和回放。
Tool 抽象实际上是 Claude Code 对“agent action”的统一工程协议。
Tool system 与 UI 也是绑定的
可以看到,Claude Code 的工具系统从一开始就把“用户如何看到工具执行过程”纳入了设计目标,而不只是把它当成后端执行逻辑。
这也是它能够做出产品级交互体验的关键原因之一。
为什么说工具系统是架构中轴
因为 Claude Code 的很多核心模块,最终都围绕 Tool 运转。prompt 要把 tool 暴露给模型,权限系统要决定 tool 能不能执行,runtime 要调度 tool 的并发与顺序,transcript 要记录 tool_use 和 tool_result,compact 要考虑哪些 tool 信息需要保留,UI 也要渲染 tool 的过程与结果。
换个说法,Tool 已经是整套系统的中心接口,不只是“附加能力层”。
源码锚点
建议重点看:
src/Tool.tssrc/tools.tssrc/hooks/useCanUseTool.tsxsrc/utils/permissions/permissions.tssrc/services/tools/toolOrchestration.tssrc/services/tools/StreamingToolExecutor.tssrc/query.ts
结语
Claude Code 之所以能从“会调模型”进化到“能执行任务”,关键就在于它把 Tool 做成了一个完整的工程系统。
理解了这一层,你就会明白:Claude Code 的真正能力并不来自某个单独工具,而是来自模型、工具、权限、上下文和消息链之间的协同。

动手练习
打开 src/Tool.ts,搜索export type Tool,浏览完整的字段列表,感受 Tool 抽象的广度打开 src/tools.ts,搜索getAllBaseTools,看哪些工具是无条件加载的,哪些是 feature-gated 的打开 src/utils/permissions/permissions.ts,搜索hasPermissionsToUseTool,跟一遍 allow/deny/ask 的分支逻辑
夜雨聆风