在 Claude Code 的 40+ 个工具背后,存在一套精密的共享基础设施。这套基础设施负责工具间的公共能力复用、Schema 缓存优化、动态工具发现,以及整个工具池的组装与过滤。如果说单个工具是战场上的士兵,那么这些共享模块就是后勤补给线、情报网络和作战指挥部——它们不直接参与"战斗",却决定了整个工具系统能否高效运转。
本文将深入解析 tools/shared/、tools/testing/、toolSchemaCache.ts、ToolSearchTool/、toolSearch.ts 以及 tools.ts 中的核心逻辑,揭示 Claude Code 如何在复杂的多工具环境中保持高性能与高可维护性。
一、tools/shared/:工具间的公共能力仓库
tools/shared/ 目录是 Claude Code 工具系统中公共逻辑的集中存放地。它的设计理念很明确:避免多个工具重复实现相同的业务逻辑,将通用能力抽象为可复用的模块。目前该目录主要包含两个核心文件:
1.1 gitOperationTracking.ts —— Git 操作统一追踪
当 Claude Code 执行 BashTool 运行 git commit、git push 或 gh pr create 时,系统需要感知这些操作以更新内部状态、触发分析事件(Analytics)和更新 OTLP 指标。gitOperationTracking.ts 就是负责这一任务的"观测站"。
// src/tools/shared/gitOperationTracking.ts, 行 28-35
function gitCmdRe(subcmd: string, suffix = ''): RegExp {
return new RegExp(
`\\bgit(?:\\s+-[cC]\\s+\\S+|\\s+--\\S+=\\S+)*\\s+${subcmd}\\b${suffix}`,
)
}这个正则构建函数的设计非常精巧:它不仅匹配 git commit,还能容忍 git -c commit.gpgsign=false commit 这类带有全局选项的命令。通过 \b 单词边界和 (?:\s+-[cC]\s+\S+|\s+--\S+=\S+)* 这段模式,它能够跳过任意数量的 -c key=val 或 --git-dir=path 参数。
detectGitOperation() 函数(约行 145-210)会同时扫描命令字符串和输出内容,返回一个结构化的操作描述对象:
// src/tools/shared/gitOperationTracking.ts, 行 145-155
export function detectGitOperation(
command: string,
output: string,
): {
commit?: { sha: string; kind: CommitKind }
push?: { branch: string }
branch?: { ref: string; action: BranchAction }
pr?: { number: number; url?: string; action: PrAction }
} {该函数不仅能识别常规的 Git 操作,还支持 gh pr create/edit/merge/comment/close/ready 等 GitHub CLI 操作,甚至能检测通过 curl 调用 REST API 创建 PR 的场景。当检测到 pr created 时,它会自动调用 linkSessionToPR() 将当前 Session 与创建的 PR 关联起来——这对于后续在对话中追踪 PR 状态至关重要。
1.2 spawnMultiAgent.ts —— 多 Agent 生成的共享逻辑
TeammateTool 和 AgentTool 都需要创建新的 Agent 实例,两者的底层逻辑有大量重叠。spawnMultiAgent.ts 将这些公共逻辑抽取出来,提供了统一的 teammate 生成能力。
核心导出函数 spawnTeammate()(约行 690-700)是外部调用的统一入口:
// src/tools/shared/spawnMultiAgent.ts, 行 690-698
export async function spawnTeammate(
config: SpawnTeammateConfig,
context: ToolUseContext,
): Promise<{ data: SpawnOutput }> {
return handleSpawn(config, context)
}handleSpawn() 内部实现了三种 spawn 模式的选择逻辑:
- In-Process 模式:当
isInProcessEnabled()返回 true 时,在同一个 Node.js 进程中通过AsyncLocalStorage创建 teammate - Split-Pane 模式(默认):在 tmux 或 iTerm2 中创建分屏视图,leader 在左、teammates 在右
- Separate-Window 模式(Legacy):为每个 teammate 创建独立的 tmux 窗口
模型继承是另一个关键设计点。resolveTeammateModel() 函数处理 inherit 别名:
// src/tools/shared/spawnMultiAgent.ts, 行 70-78
export function resolveTeammateModel(
inputModel: string | undefined,
leaderModel: string | null,
): string {
if (inputModel === 'inherit') {
return leaderModel ?? getDefaultTeammateModel(leaderModel)
}
return inputModel ?? getDefaultTeammateModel(leaderModel)
}这里有一个值得注意的 bugfix:早期版本中 'inherit' 被直接传递给 --model CLI 参数,导致模型报错"It may not exist or you may not have access"。现在的实现会正确将其解析为 leader 的模型名或 fallback 模型。
buildInheritedCliFlags() 函数则确保子 Agent 继承父级的重要配置——包括权限模式(--dangerously-skip-permissions)、模型选择(--model)、设置路径(--settings)和插件目录(--plugin-dir)。但有一个安全例外:当 plan_mode_required 为 true 时,子 Agent 不会继承 bypass permissions 标志,因为 Plan 模式在安全优先级上高于权限绕过。
二、tools/testing/:工具测试框架
测试框架位于 tools/testing/ 目录,目前核心是一个专门用于测试的工具:TestingPermissionTool。
// src/tools/testing/TestingPermissionTool.tsx, 行 1-25
/**
* This testing-only tool will always pop up a permission dialog when called by
* the model.
*/
import { z } from 'zod/v4';
import type { Tool } from '../../Tool.js';
import { buildTool, type ToolDef } from '../../Tool.js';
import { lazySchema } from '../../utils/lazySchema.js';
const NAME = 'TestingPermission';
const inputSchema = lazySchema(() => z.strictObject({}));这个工具的设计目的很明确:验证权限系统的端到端行为。它的关键特性包括:
- 始终请求权限:
checkPermissions()永远返回{ behavior: 'ask', message: 'Run test?' } - 仅在测试环境启用:
isEnabled()返回"production" === 'test',即只有在NODE_ENV=test时才会被注册到工具池中 - 无副作用:
isReadOnly()返回 true,call()仅返回成功字符串
// src/tools/testing/TestingPermissionTool.tsx, 行 40-48
async checkPermissions() {
// This tool always requires permission
return {
behavior: 'ask' as const,
message: `Run test?`
};
}在 tools.ts 中,它的注册条件非常严格:
// src/tools.ts, 行 130
...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []),这种设计保证了生产环境绝不会暴露测试工具,同时测试环境又能有一个"永远触发权限对话框"的受控目标。端到端测试可以利用它验证:
- 权限对话框是否正确渲染
- 用户允许/拒绝后工具是否正确执行/取消
- 权限规则(alwaysAllow/alwaysDeny)是否按预期生效
三、Schema 缓存:toolSchemaCache.ts
工具 Schema 的渲染代价极高。在 Claude Code 中,所有 40+ 个工具的 JSON Schema 定义合计约 11K tokens,且这些 Schema 被放置在 system prompt 的 position 2 位置——这意味着任何微小的 Schema 变化都会导致整个工具块及下游所有内容的缓存失效。toolSchemaCache.ts 就是为了解决这个问题而存在的。
// src/utils/toolSchemaCache.ts, 行 1-20
import type { BetaTool } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
// Session-scoped cache of rendered tool schemas. Tool schemas render at server
// position 2 (before system prompt), so any byte-level change busts the entire
// ~11K-token tool block AND everything downstream.
type CachedSchema = BetaTool & {
strict?: boolean
eager_input_streaming?: boolean
}
const TOOL_SCHEMA_CACHE = new Map<string, CachedSchema>()这个缓存有两个核心设计原则:
1. Session 级别的作用域
缓存是一个模块级全局的 Map,在单次 Session 内持续有效。这意味着:
- GrowthBook 功能开关在中途翻转(如
tengu_tool_pear、tengu_fgts)不会导致已缓存的 Schema 失效 - MCP 服务器重连后,新工具的 Schema 会走首次渲染路径,而已缓存的 built-in 工具不受影响
2. 显式清除接口
// src/utils/toolSchemaCache.ts, 行 16-20
export function getToolSchemaCache(): Map<string, CachedSchema> {
return TOOL_SCHEMA_CACHE
}
export function clearToolSchemaCache(): void {
TOOL_SCHEMA_CACHE.clear()
}clearToolSchemaCache() 被设计为 auth.ts 等叶子模块可以安全调用,而不需要引入 api.ts(否则会因为 plans→settings→file→growthbook→config→bridgeEnabled→auth 的依赖链产生循环导入)。
这个简单的两函数接口背后蕴含着深刻的架构考量:缓存策略宁可保守也不要激进。因为一次不必要的缓存失效意味着 11K tokens 的重新传输——这在 latency 敏感的场景下代价极高。
四、工具搜索:ToolSearchTool
当 MCP 服务器接入后,Claude Code 的工具数量可能从 40 个暴涨到上百个。如果将所有工具的完整 Schema 都塞进 system prompt,上下文会被迅速耗尽。ToolSearchTool 就是解决这个问题的动态加载机制。
4.1 核心机制:Deferred Loading
isDeferredTool() 函数(位于 prompt.ts)定义了哪些工具需要延迟加载:
// src/tools/ToolSearchTool/prompt.ts, 行 55-75
export function isDeferredTool(tool: Tool): boolean {
// Explicit opt-out via _meta['anthropic/alwaysLoad']
if (tool.alwaysLoad === true) return false
// MCP tools are always deferred (workflow-specific)
if (tool.isMcp === true) return true
// Never defer ToolSearch itself
if (tool.name === TOOL_SEARCH_TOOL_NAME) return false
// Fork-first experiment: Agent must be available turn 1
if (feature('FORK_SUBAGENT') && tool.name === AGENT_TOOL_NAME) {
// ...
}
return tool.shouldDefer === true
}延迟加载遵循三条规则:
- MCP 工具默认延迟加载:它们是工作流特定的,不同用户接入的 MCP 服务器各不相同
- 显式 opt-out:通过
alwaysLoad: true或_meta['anthropic/alwaysLoad']可以强制预加载 - 核心工具永不延迟:
ToolSearchTool自身、AgentTool(在 fork 实验下)、BriefTool等通信通道必须立即可用
4.2 搜索实现
ToolSearchTool.ts 实现了两种查询模式:
精确选择模式:select:ToolNameA,ToolNameB
// src/tools/ToolSearchTool/ToolSearchTool.ts, 行 210-235
const selectMatch = query.match(/^select:(.+)$/i)
if (selectMatch) {
const requested = selectMatch[1]!
.split(',')
.map(s => s.trim())
.filter(Boolean)
const found: string[] = []
const missing: string[] = []
for (const toolName of requested) {
const tool =
findToolByName(deferredTools, toolName) ??
findToolByName(tools, toolName)
if (tool) {
if (!found.includes(tool.name)) found.push(tool.name)
} else {
missing.push(toolName)
}
}关键词搜索模式:支持自然语言查询
// src/tools/ToolSearchTool/ToolSearchTool.ts, 行 245-260
const matches = await searchToolsWithKeywords(
query,
deferredTools,
tools,
max_results,
)searchToolsWithKeywords() 的评分算法(约行 125-200)非常精巧:
| 匹配类型 | MCP 工具得分 | 普通工具得分 | 说明 |
|---|---|---|---|
| 名称部分精确匹配 | 12 | 10 | 如 mcp__slack__send 中的 slack |
| 名称部分包含匹配 | 6 | 5 | 如 send_message 中的 send |
| 完整名称包含 | 3 | 3 | 兜底匹配 |
| searchHint 匹配 | 4 | 4 | 人工标注的能力短语 |
| 描述词边界匹配 | 2 | 2 | 基于 \bterm\b 的正则匹配 |
这种多层级的评分机制确保了模型搜索 "slack" 时,Slack MCP 工具会排在最前面,而不是某个描述里偶然提到 "slack" 的 built-in 工具。
搜索结果以 tool_reference 块的形式返回给模型:
// src/tools/ToolSearchTool/ToolSearchTool.ts, 行 275-290
mapToolResultToToolResultBlockParam(content: Output, toolUseID: string) {
return {
type: 'tool_result',
tool_use_id: toolUseID,
content: content.matches.map(name => ({
type: 'tool_reference' as const,
tool_name: name,
})),
} as unknown as ToolResultBlockParam
}Anthropic API 会在服务端将这些 tool_reference 展开为完整的工具定义,模型无需额外的往返即可调用这些工具。
4.3 阈值自动判断
toolSearch.ts 中的 isToolSearchEnabled() 函数是决定是否启用延迟加载的"大脑"。它支持三种模式:
tst(Tool Search Tool):始终启用延迟加载tst-auto:仅当延迟工具的描述文本超过上下文窗口的 10%(可配置)时才启用standard:完全禁用延迟加载
// src/utils/toolSearch.ts, 行 95-125
export function getToolSearchMode(): ToolSearchMode {
// Kill switch: disable beta features for proxy gateways
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS)) {
return 'standard'
}
const value = process.env.ENABLE_TOOL_SEARCH
// auto:N syntax: auto:0 = always, auto:100 = never
const autoPercent = value ? parseAutoPercentage(value) : null
if (autoPercent === 0) return 'tst'
if (autoPercent === 100) return 'standard'
if (isAutoToolSearchMode(value)) return 'tst-auto'
if (isEnvTruthy(value)) return 'tst'
if (isEnvDefinedFalsy(value)) return 'standard'
return 'tst' // default: always defer MCP and shouldDefer tools
}tst-auto 模式的阈值检查优先使用精确的 token 计数 API(通过 countToolDefinitionTokens()),当 API 不可用时回退到字符数启发式(CHARS_PER_TOKEN = 2.5)。
五、工具池组装:assembleToolPool
工具池组装是整个工具系统的"总装车间"。Claude Code 的工具来源很复杂:built-in 工具、MCP 动态工具、Feature Flag 条件工具、环境变量开关工具等。assembleToolPool() 负责将这些来源统一合并为模型最终看到的工具列表。
5.1 从 getAllBaseTools 到 getTools
getAllBaseTools()(tools.ts 约行 160-210)是所有可能被注册的工具的"全量清单":
// src/tools.ts, 行 160-210
export function getAllBaseTools(): Tools {
return [
AgentTool,
TaskOutputTool,
BashTool,
...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
ExitPlanModeV2Tool,
FileReadTool,
// ... 约 40 个工具
...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
]
}注意几个条件注册的例子:
hasEmbeddedSearchTools()为 true 时(原生构建内置了 bfs/ugrep),GlobTool和GrepTool被省略process.env.USER_TYPE === 'ant'控制的内部工具只在 Anthropic 内部环境出现feature('AGENT_TRIGGERS')等 Feature Flag 控制实验性工具
getTools() 在此基础上进行两层过滤:
filterToolsByDenyRules():根据权限上下文中的 deny 规则剔除工具(包括mcp__server前缀规则可以批量屏蔽某个 MCP 服务器的所有工具)isEnabled():每个工具的自检逻辑(如TestingPermissionTool只在NODE_ENV === 'test'时启用)
5.2 assembleToolPool:最终的合并与去重
// src/tools.ts, 行 280-310
export function assembleToolPool(
permissionContext: ToolPermissionContext,
mcpTools: Tools,
): Tools {
const builtInTools = getTools(permissionContext)
// Filter out MCP tools that are in the deny list
const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
// Sort each partition for prompt-cache stability, keeping built-ins as a
// contiguous prefix. The server's claude_code_system_cache_policy places a
// global cache breakpoint after the last prefix-matched built-in tool.
const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
return uniqBy(
[...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
'name',
)
}这里有三个关键设计决策:
分区排序(Partition Sort)
工具数组被分为 built-in 和 MCP 两个分区,各自按名称排序后再拼接。这保证了 built-in 工具始终位于数组的前缀位置。为什么这很重要?因为 Anthropic 服务端的 claude_code_system_cache_policy 在最后一个前缀匹配的 built-in 工具后放置全局缓存断点。如果 flat sort 将 MCP 工具穿插进 built-in 之间,每次 MCP 工具变化都会破坏所有下游内容的缓存键。
去重优先级(Deduplication Priority)
uniqBy([...builtInTools, ...allowedMcpTools], 'name') 使用 lodash 的 uniqBy,它保留首次出现的元素。由于 built-in 工具排在 MCP 工具前面,这意味着 built-in 工具在命名冲突时具有更高的优先级——这是有意为之的安全设计。
MCP 过滤前置
filterToolsByDenyRules() 在 MCP 工具进入组装流程之前就将其过滤掉。这确保了被 deny 规则屏蔽的 MCP 工具既不会出现在 system prompt 中,也不会被 ToolSearchTool 搜索到——模型从头到尾都不知道这些工具的存在。
5.3 mergeAndFilterTools:REPL 环境的二次加工
flowchart TD
A[getAllBaseTools] --> B[getTools]
B -->|过滤 deny 规则 + isEnabled| C[builtInTools]
D[MCP 服务器] --> E[mcpTools]
E -->|filterToolsByDenyRules| F[allowedMcpTools]
C --> G[assembleToolPool]
F --> G
G -->|分区排序 + uniqBy| H[assembledPool]
I[initialTools] --> J[mergeAndFilterTools]
H --> J
J -->|分区排序 + uniqBy| K[mergedPool]
K -->|COORDINATOR_MODE?| L[applyCoordinatorToolFilter]
L --> M[最终工具列表]mergeAndFilterTools()(位于 toolPool.ts)是 REPL 路径特有的二次加工步骤:
// src/utils/toolPool.ts, 行 35-55
export function mergeAndFilterTools(
initialTools: Tools,
assembled: Tools,
mode: ToolPermissionContext['mode'],
): Tools {
// Merge initialTools on top - they take precedence in deduplication.
const [mcp, builtIn] = partition(
uniqBy([...initialTools, ...assembled], 'name'),
isMcpTool,
)
const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
const tools = [...builtIn.sort(byName), ...mcp.sort(byName)]
if (feature('COORDINATOR_MODE') && coordinatorModeModule) {
if (coordinatorModeModule.isCoordinatorMode()) {
return applyCoordinatorToolFilter(tools)
}
}
return tools
}initialTools 通常来自 REPL 启动时传入的 props(如 --tools 命令行参数指定的额外工具)。它们通过相同的 uniqBy 机制获得最高优先级——因为它们排在数组最前面。
当 Coordinator Mode 启用时,applyCoordinatorToolFilter() 会将工具列表严格限制为:
// src/constants/tools.ts, 行 75-80
export const COORDINATOR_MODE_ALLOWED_TOOLS = new Set([
AGENT_TOOL_NAME,
TASK_STOP_TOOL_NAME,
SEND_MESSAGE_TOOL_NAME,
SYNTHETIC_OUTPUT_TOOL_NAME,
])这意味着在 Coordinator 模式下,Worker Agent 只能使用 Agent 管理、任务控制和消息发送工具——所有的文件操作、Shell 执行等"危险"工具都被强制移除。
六、Deferred Tools Delta:跨轮次的状态同步
当工具搜索启用时,还有一个精妙的状态同步机制:getDeferredToolsDelta()(位于 toolSearch.ts,约行 340-400)。
它的作用是在对话的每一轮之间追踪"哪些延迟加载的工具已经被宣布给模型"。通过扫描消息历史中的 deferred_tools_delta attachment,它可以计算出:
- Added:本轮新出现的延迟工具(如新连接的 MCP 服务器)
- Removed:从工具池中彻底消失的延迟工具(MCP 服务器断开)
// src/utils/toolSearch.ts, 行 340-360
export function getDeferredToolsDelta(
tools: Tools,
messages: Message[],
scanContext?: DeferredToolsDeltaScanContext,
): DeferredToolsDelta | null {
const announced = new Set<string>()
// Scan prior deferred_tools_delta attachments
for (const msg of messages) {
if (msg.type !== 'attachment') continue
if (msg.attachment.type !== 'deferred_tools_delta') continue
for (const n of msg.attachment.addedNames) announced.add(n)
for (const n of msg.attachment.removedNames) announced.delete(n)
}
// ... diff logic
}scanContext 参数是一个调用点标识符(如 attachments_main、attachments_subagent、compact_full 等),它使得开发者可以在 BigQuery 中精确区分不同调用路径的统计特征——这对于定位跨轮次状态同步的 bug 至关重要。
七、总结
Claude Code 的工具共享基础设施是一个精心设计的分层系统:
| 层级 | 模块 | 职责 |
|---|---|---|
| 公共能力层 | tools/shared/ | Git 操作追踪、多 Agent 生成共享 |
| 质量保障层 | tools/testing/ | 受控的测试工具,仅在测试环境暴露 |
| 缓存优化层 | toolSchemaCache.ts | Session 级 Schema 缓存,避免 11K tokens 的重复渲染 |
| 动态发现层 | ToolSearchTool/ + toolSearch.ts | Deferred loading、关键词搜索、阈值自动判断 |
| 总装集成层 | tools.ts + toolPool.ts | 工具池组装、去重、权限过滤、Coordinator 模式裁剪 |
这些基础设施不直接面向用户,却是 Claude Code 能够在 40+ built-in 工具 + 任意数量 MCP 工具的复杂环境下保持高性能、高可维护性的关键。特别是 Schema 缓存的分区排序策略、ToolSearch 的延迟加载机制,以及工具池组装的去重优先级设计,都体现了"缓存稳定性"和"安全边界"在工程实践中的深度考量。
理解这些底层支撑,不仅有助于我们阅读 Claude Code 的源码,也能为设计自己的 AI Agent 工具系统提供宝贵的架构参考。