Claude Code 源码分析系列文章:
- 初始化及 Ink 系统
- 核心对话循环
- [Tool/MCP/Skill 可扩展工具系统](#Tool/MCP/Skill 可扩展工具系统 "#")
本文基于项目实际源码,深入分析 Claude Code 的工具系统架构。涵盖 Tool 类型定义、工具注册与发现、执行管道、并发控制、MCP 协议集成、Skill/Command 体系及 SkillTool 模型驱动调用的完整链路。
一、架构总览
Claude Code 的工具系统是一个三层可扩展架构:内置工具 (Built-in Tools) 提供文件读写、Bash 执行等基础能力;MCP 工具 通过标准协议接入外部服务;Skill/Command 提供用户可定义的高级行为模板。三者通过统一的 Tool 接口抽象,共享同一套注册、发现、权限、执行管道。
src/Tool.ts"] B["buildTool() 工厂
应用默认值"] end subgraph 注册层 direction LR C["getAllBaseTools()
61+ 内置工具"] D["MCP Client
mcp__server__tool"] E["Skill Loader
managed/user/project"] end subgraph 过滤层 direction LR F["filterToolsByDenyRules()"] G["getTools() --- isEnabled 过滤"] H["assembleToolPool() --- 合并去重"] end subgraph 执行层 direction LR I["runToolUse() --- 入口"] J["checkPermissionsAndCallTool()
9 阶段管道"] K["StreamingToolExecutor
并发 + 有序产出"] end A --> B B --> C D --> H E --> H C --> F --> G --> H H --> I --> J J --> K
核心设计原则:
- 统一接口 :所有工具(内置 / MCP / Skill)都实现同一个
Tool泛型接口 - 权限前置:工具执行前必须通过多层权限检查(配置规则 → Hooks → 用户确认)
- 并发安全 :通过
isConcurrencySafe标记控制工具并发策略 - 可扩展:MCP 协议和 Skill 目录允许用户自行扩展工具集
二、Tool 类型系统
源码位置:
src/Tool.ts
Tool 类型系统是整个工具架构的基石,由四个核心类型组成。
2.1 Tool<Input, Output, P> --- 核心泛型
Tool 是所有工具的统一接口,定义了约 40 个属性和方法:
typescript
// src/Tool.ts:362
export type Tool<
Input extends AnyObject = AnyObject,
Output = unknown,
P extends ToolProgressData = ToolProgressData,
> = {
// ========= 身份标识 =========
name: string // 唯一名称,如 "Bash", "mcp__ide__getDiagnostics"
aliases?: string[] // 别名,兼容旧名称
userFacingName: (input: Input) => string // UI 显示名
userFacingNameBackgroundColor?( // UI 显示名称的颜色
input: Partial<z.infer<Input>> | undefined,
): keyof Theme | undefined
// ========= Schema =========
inputSchema: Input // Zod schema,用于输入验证
outputSchema?: z.ZodType<unknown> // 输出类型(可选)
// ========= 元信息 =========
description(
input: z.infer<Input>,
options: {
isNonInteractiveSession: boolean
toolPermissionContext: ToolPermissionContext
tools: Tools
},
): Promise<string> // 工具描述(送入 system prompt)
prompt(options: {
getToolPermissionContext: () => Promise<ToolPermissionContext>
tools: Tools
agents: AgentDefinition[]
allowedAgentTypes?: string[]
}): Promise<string> // 使用提示
searchHint?: string // 工具搜索的额外匹配词
// ========= 能力标记 =========
isEnabled(): boolean // 当前环境是否可用
isReadOnly(): boolean // 是否只读(影响权限策略)
isDestructive?(): boolean // 是否破坏性操作
isConcurrencySafe(input: z.infer<Input>): boolean // 是否可并发执行
isSearchOrReadCommand?(input: z.infer<Input>): { // 是否查询类工具
isSearch: boolean
isRead: boolean
isList?: boolean
}
isOpenWorld?(input: z.infer<Input>): boolean // 输入是否来自外部
// ========= 执行 =========
call(
args: z.infer<Input>,
context: ToolUseContext,
canUseTool: CanUseToolFn,
parentMessage: AssistantMessage,
onProgress?: ToolCallProgress<P>,
): Promise<ToolResult<Output>> // 核心执行函数
// ========= 权限 =========
async checkPermissions(
input: z.infer<Input>,
context: ToolUseContext,
): Promise<PermissionResult> // 权限检查
validateInput?(
input: Input,
context: ToolUseContext,
): Promise<ValidationResult> // 业务级输入校验
// ========= 中断策略 =========
interruptBehavior?(): 'cancel' | 'block'
// ========= UI 渲染 =========
renderToolUseMessage(props): React.ReactNode
renderToolResultMessage(props): React.ReactNode
renderToolUseProgressMessage?(props): React.ReactNode
// ========= 高级特性 =========
maxResultSizeChars?: number // 结果截断阈值
strict?: boolean // 严格模式
isMcp?: boolean // 标记为 MCP 工具
isLsp?: boolean // 标记为 LSP 工具
shouldDefer?: boolean // 延迟加载(工具搜索时才启用)
alwaysLoad?: boolean // 始终加载
mcpInfo?: { serverName: string; toolName: string } // MCP 来源信息
maxResultSizeChars: number // 工具最长输出,超出原始内容存储到本地文件,返回特定提示词及压缩后的结果。默认50_000
backfillObservableInput?( // 对输入做浅拷贝供 hooks 观察
input: Input,
): Record<string, unknown> | undefined
// ...
}
2.2 ToolResult --- 工具返回值
工具执行完成后返回的统一结构:
typescript
// src/Tool.ts:321-336
export type ToolResult<T> = {
data: T // 实际输出数据
newMessages?: ( // 注入额外消息到对话流
| UserMessage
| AssistantMessage
| AttachmentMessage
| SystemMessage
)[]
contextModifier?: ( // 修改后续工具的上下文
context: ToolUseContext,
) => ToolUseContext
mcpMeta?: { // MCP 元数据
_meta?: Record<string, unknown>
structuredContent?: Record<string, unknown>
}
}
contextModifier 是一个精妙的设计:工具可以通过返回值修改后续执行的上下文。例如 EnterPlanModeTool 执行后可通过 contextModifier 切换权限模式。
2.3 ToolUseContext --- 执行上下文
typescript
// src/Tool.ts:158
export type ToolUseContext = {
options: {
commands: Command[] // 可用命令列表
tools: Tools // 可用工具列表
mcpClients: MCPServerConnection[] // MCP 连接
mcpResources: Record<string, ServerResource[]>
mainLoopModel: string // 主循环模型
thinkingConfig: ThinkingConfig // 思考模式
agentDefinitions: AgentDefinitionsResult
maxBudgetUsd?: number
querySource?: QuerySource
refreshTools?: () => Tools // 动态刷新工具列表
// ...
}
abortController: AbortController // 中止控制器
readFileState: FileStateCache // 文件状态缓存
getAppState(): AppState // 读取全局状态
setAppState(f: (prev: AppState) => AppState): void // 修改全局状态
requestPrompt?: PermissionRequestFn // 请求用户权限
// ...
}
2.4 ToolPermissionContext --- 权限上下文
typescript
// src/Tool.ts:123-138
export type ToolPermissionContext = DeepImmutable<{
mode: PermissionMode // 'default' | 'plan' | ...
additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>
alwaysAllowRules: ToolPermissionRulesBySource // 白名单规则
alwaysDenyRules: ToolPermissionRulesBySource // 黑名单规则
alwaysAskRules: ToolPermissionRulesBySource // 始终询问规则
isBypassPermissionsModeAvailable: boolean
isAutoModeAvailable?: boolean
strippedDangerousRules?: ToolPermissionRulesBySource
shouldAvoidPermissionPrompts?: boolean
awaitAutomatedChecksBeforeDialog?: boolean
prePlanMode?: PermissionMode
}>
DeepImmutable 保证权限上下文在传递过程中不可被意外修改,是安全性的重要保障。
2.5 buildTool() --- 工厂函数与默认值
typescript
// src/Tool.ts (TOOL_DEFAULTS)
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: () => false, // 默认不可并发
isReadOnly: () => false,
isDestructive: () => false,
isOpenWorld: () => false,
// ...
}
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
return {
...TOOL_DEFAULTS,
userFacingName: () => def.name,
...def,
} as BuiltTool<D>
}
buildTool() 将用户定义与默认值合并,确保每个工具都有完整的接口实现。工具作者只需关注核心逻辑(name、inputSchema、call()等),其余属性自动填充。
2.6 工具查找辅助函数
typescript
// src/Tool.ts:348
export function toolMatchesName(
tool: { name: string; aliases?: string[] },
name: string,
): boolean {
return tool.name === name || (tool.aliases?.includes(name) ?? false)
}
export function findToolByName(
tools: Tools,
name: string,
): Tool | undefined {
return tools.find(t => toolMatchesName(t, name))
}
别名机制允许工具改名时保持向后兼容(如旧版工具名 → 新名称映射)。
三、工具注册与发现
源码位置:
src/tools.ts
工具注册是一条多级过滤管道:从全量工具列表开始,逐步筛选出当前环境可用的工具集。
3.1 getAllBaseTools() --- 全量工具清单
typescript
// src/tools.ts:191
export function getAllBaseTools(): Tools {
return [
AgentTool,
TaskOutputTool,
BashTool,
// 嵌入式搜索工具可用时跳过 Glob/Grep
...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
ExitPlanModeV2Tool,
FileReadTool,
FileEditTool,
FileWriteTool,
NotebookEditTool,
WebFetchTool,
TodoWriteTool,
WebSearchTool,
TaskStopTool,
AskUserQuestionTool,
SkillTool,
EnterPlanModeTool,
// 条件加载 ------ 基于环境变量
...(process.env.USER_TYPE === 'ant' ? [ConfigTool] : []),
...(process.env.USER_TYPE === 'ant' ? [TungstenTool] : []),
...(SuggestBackgroundPRTool ? [SuggestBackgroundPRTool] : []),
// 条件加载 ------ 基于 feature flag
...(WebBrowserTool ? [WebBrowserTool] : []),
...(isTodoV2Enabled()
? [TaskCreateTool, TaskGetTool, TaskUpdateTool, TaskListTool]
: []),
...(isWorktreeModeEnabled() ? [EnterWorktreeTool, ExitWorktreeTool] : []),
...(SleepTool ? [SleepTool] : []),
...cronTools,
...(RemoteTriggerTool ? [RemoteTriggerTool] : []),
BriefTool,
// 测试专用
...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []),
ListMcpResourcesTool,
ReadMcpResourceTool,
...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
]
}
工具按以下维度条件加载:
| 条件类型 | 示例 | 机制 |
|---|---|---|
| Feature Flag | WebBrowserTool、SleepTool |
feature('FLAG_NAME') + 运行时 require() |
| 环境变量 | ConfigTool、TungstenTool |
process.env.USER_TYPE === 'ant' |
| 运行时检测 | GlobTool、GrepTool、TaskCreateTool |
hasEmbeddedSearchTools()、isTodoV2Enabled() |
3.2 过滤管道
30+ 工具"] --> B["filterToolsByDenyRules()
配置黑名单"] B --> C["isEnabled() 检查
环境可用性"] C --> D["assembleToolPool()
合并 MCP 工具"]
filterToolsByDenyRules() --- 配置级黑名单
typescript
// src/tools.ts:260
export function filterToolsByDenyRules<T extends {
name: string
mcpInfo?: { serverName: string; toolName: string }
}>(tools: readonly T[], permissionContext: ToolPermissionContext): T[] {
return tools.filter(tool => !getDenyRuleForTool(permissionContext, tool))
}
该函数不仅匹配工具精确名称,还支持 MCP 前缀规则:配置 mcp__server 可以一次性屏蔽整个 MCP Server 的所有工具。
getTools() --- 完整过滤链
typescript
// src/tools.ts:269-325
export const getTools = (permissionContext: ToolPermissionContext): Tools => {
// 1. 简单模式:只保留 Bash/Read/Edit
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
const simpleTools: Tool[] = [BashTool, FileReadTool, FileEditTool]
return filterToolsByDenyRules(simpleTools, permissionContext)
}
// 2. 排除特殊工具(MCP Resources、Synthetic Output)
const specialTools = new Set([
ListMcpResourcesTool.name,
ReadMcpResourceTool.name,
SYNTHETIC_OUTPUT_TOOL_NAME,
])
const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name))
// 3. 应用黑名单过滤
let allowedTools = filterToolsByDenyRules(tools, permissionContext)
// 4. REPL 模式下隐藏被 REPL 包装的原始工具
if (isReplModeEnabled()) {
const replEnabled = allowedTools.some(
tool => toolMatchesName(tool, REPL_TOOL_NAME),
)
if (replEnabled) {
allowedTools = allowedTools.filter(
tool => !REPL_ONLY_TOOLS.has(tool.name),
)
}
}
// 5. isEnabled() 检查
const isEnabled = allowedTools.map(_ => _.isEnabled())
return allowedTools.filter((_, i) => isEnabled[i])
// 这段代码给我看懵了,不知道是不是AI写的,可以简写为 allowedTools.filter(_ => _.isEnabled())
}
3.3 assembleToolPool() --- 内置工具与 MCP 工具合并
typescript
// src/tools.ts:343-365
export function assembleToolPool(
permissionContext: ToolPermissionContext,
mcpTools: Tools,
): Tools {
const builtInTools = getTools(permissionContext)
const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
// 分区排序:内置工具在前,MCP 工具在后
// 保证 prompt cache 稳定性
const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
return uniqBy(
[...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
'name',
)
}
排序策略的关键考量:API 端对 system prompt 做了 cache 分段(claude_code_system_cache_policy),将 cache 断点放在最后一个内置工具之后。如果用简单的全局排序,MCP 工具会穿插到内置工具之间,导致每次 MCP 工具变化都使所有下游 cache key 失效。分区排序 + uniqBy 保证:
- 内置工具始终形成稳定的前缀块
- 同名冲突时内置工具优先(
uniqBy保留第一个) - MCP 工具在后缀块中独立排序
四、工具执行管道
源码位置:
src/services/tools/toolExecution.ts
工具执行是一条 9 阶段的异步管道,从模型返回的 tool_use 块开始,到工具结果注入对话流结束。
4.1 runToolUse() --- 管道入口
typescript
// src/services/tools/toolExecution.ts:337
export async function* runToolUse(
toolUse: ToolUseBlock,
assistantMessage: AssistantMessage,
canUseTool: CanUseToolFn,
toolUseContext: ToolUseContext,
): AsyncGenerator<Message> {
const toolName = toolUse.name
// 1. 在当前工具列表中查找
let tool = findToolByName(toolUseContext.options.tools, toolName)
// 2. 回退:从全量工具列表中按别名查找
if (!tool) {
const fallbackTool = findToolByName(getAllBaseTools(), toolName)
if (fallbackTool && fallbackTool.aliases?.includes(toolName)) {
tool = fallbackTool
}
}
// 3. 工具未找到 → 返回错误消息
if (!tool) {
yield createToolResultMessage(/* error: tool not found */)
return
}
// 4. 中止检查
if (toolUseContext.abortController.signal.aborted) {
yield createToolResultMessage(/* error: aborted */)
return
}
// 5. 委托给 streamedCheckPermissionsAndCallTool
for await (const update of streamedCheckPermissionsAndCallTool(
tool,
toolUse.id,
toolInput,
toolUseContext,
canUseTool,
assistantMessage,
messageId,
requestId,
mcpServerType,
mcpServerBaseUrl,
)) {
yield update
}
}
别名回退机制是一个防御性设计:即使工具被重命名或从当前列表中移除,模型仍可能使用旧名称调用。通过 aliases 字段实现平滑迁移。
4.2 checkPermissionsAndCallTool() --- 9 阶段管道
类型校验"] --> B["② validateInput
业务校验"] B --> C["③ 推测性分类
(仅 Bash)"] C --> D["④ 剥离内部字段
(安全防御)"] D --> E["⑤ backfillObservableInput
浅拷贝供 hooks"] E --> F["⑥ PreToolUse Hooks
外部拦截"] F --> G["⑦ resolveHookPermission
综合决策"] G --> H{"权限通过?"} H -->|是| I["⑧ tool.call()
实际执行"] H -->|否| J["⑨ 权限拒绝处理
PermissionDenied Hooks"] I --> K["PostToolUse Hooks"] style A fill:#e8f5e9 style F fill:#fff3e0 style I fill:#e1f5fe style J fill:#ffebee
阶段 ①:Zod Schema 校验
typescript
// src/services/tools/toolExecution.ts:615
const parsedInput = tool.inputSchema.safeParse(input)
if (!parsedInput.success) {
// 返回格式化的 Zod 错误信息
yield createToolResultMessage(/* schema validation error */)
return
}
所有工具的 inputSchema 都是 Zod schema,在执行前自动校验输入类型。模型生成的 JSON 如果不符合 schema(如缺少必填字段、类型不匹配),会直接返回错误消息给模型。
阶段 ②:业务级校验
typescript
// src/services/tools/toolExecution.ts:683
const validationError = tool.validateInput?.(parsedInput.data, toolUseContext)
if (validationError) {
yield createToolResultMessage(/* validation error */)
return
}
validateInput 提供了 schema 之外的业务校验。例如 BashTool 可以在这里检查命令是否包含危险操作,FileEditTool 可以验证文件路径是否在工作目录范围内。
阶段 ③:推测性分类(Bash 专用)
typescript
// src/services/tools/toolExecution.ts:740-752
// 仅对 Bash 工具启动并行分类检查
const speculativeResult = startSpeculativeClassifierCheck(/*...*/)
在等待用户权限确认的同时,对 Bash 命令预分类(安全/危险),减少用户感知延迟。
阶段 ④:内部字段剥离(安全防御)
typescript
// src/services/tools/toolExecution.ts:761-773
// Defense-in-depth: 剥离 _simulatedSedEdit 等内部字段
// 防止模型注入内部控制参数
这是一个纵深防御措施:即使模型在输入中包含了内部控制字段,也会在执行前被清除。
阶段 ⑤:backfillObservableInput
typescript
// src/services/tools/toolExecution.ts:784-793
const observableInput = tool.backfillObservableInput?.(parsedInput.data)
创建输入的浅拷贝,供 PreToolUse hooks 观察。这样 hooks 可以读取完整的工具输入,但无法修改原始数据。
Hooks机制参考官方文档
阶段 ⑥:PreToolUse Hooks
typescript
// src/services/tools/toolExecution.ts:800-862
for await (const hookResult of runPreToolUseHooks(/*...*/)) {
// hookResult 可能包含:
// - hookPermissionResult: 权限决策
// - hookUpdatedInput: 修改后的输入
// - preventContinuation: 阻止继续
// - stop: 停止
// - additionalContext: 附加上下文
}
PreToolUse hooks 是用户自定义的 shell 脚本,在工具执行前运行。它们可以:
- 修改输入:例如在文件路径前添加前缀
- 注入上下文:向对话流添加额外信息
- 阻止执行:返回权限拒绝
- 完全停止:取消整个工具调用
阶段 ⑦:权限综合决策
typescript
// src/services/tools/toolExecution.ts:921-931
const resolved = await resolveHookPermissionDecision(
hookPermissionResult,
tool,
processedInput,
toolUseContext,
canUseTool,
assistantMessage,
toolUseID,
)
const permissionDecision = resolved.decision
processedInput = resolved.input
综合三个来源的权限判断:Hook 的决策、推测性分类结果、配置文件规则,得出最终的权限决定。
用户授权也是在此过程中拉起的

阶段 ⑧/⑨:执行或拒绝
权限拒绝时创建错误消息,并触发 PermissionDenied hooks。权限通过后调用 tool.call(),执行完成后运行 PostToolUse hooks。
如果最终获取的权限不为allow,则会构建一些消息告诉模型并返回,比如上诉截图里我选择了No,则:
typescript
// src/services/tools/toolExecution.ts:1064-1071
resultingMessages.push({
message: createUserMessage({
content: messageContent, // "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed."
imagePasteIds: rejectImageIds,
toolUseResult: `Error: ${errorMessage}`,
sourceToolAssistantUUID: assistantMessage.uuid,
}),
})
模型收到消息后则会走对话中止流程,参考核心对话循环
typescript
// 执行路径 (src/services/tools/toolExecution.ts:1207)
const result = await tool.call(
callInput,
{
...toolUseContext,
toolUseId: toolUseID,
userModified: permissionDecision.userModified ?? false,
},
canUseTool,
assistantMessage,
progress => { // 更新工具执行过程
onToolProgress({
toolUseID: progress.toolUseID,
data: progress.data,
})
},
)
// 注入 newMessages
resultingMessages.push({
message: createUserMessage({
content: contentBlocks,
imagePasteIds: allowImageIds,
toolUseResult:
toolUseContext.agentId && !toolUseContext.preserveToolUseResults
? undefined
: toolUseResult,
mcpMeta: toolUseContext.agentId ? undefined : mcpMeta,
sourceToolAssistantUUID: assistantMessage.uuid,
}),
// 处理 contextModifier,修改后续上下文
contextModifier: toolContextModifier
? {
toolUseID: toolUseID,
modifyContext: toolContextModifier,
}
: undefined,
})
// 运行 PostToolUse hooks
for await (const hookResult of runPostToolUseHooks(/*...*/)) {
//...
}
五、StreamingToolExecutor 并发执行
源码位置:
src/services/tools/StreamingToolExecutor.ts
当模型在一个响应中返回多个 tool_use 块时,StreamingToolExecutor 负责决定哪些工具可以并行执行、哪些必须串行排队。
5.1 TrackedTool 状态模型
TrackedTool通过执行工具的isConcurrencySafe获得当前工具是否支持并行。
typescript
// src/services/tools/StreamingToolExecutor.ts:21-32
type TrackedTool = {
id: string // tool_use block ID
block: ToolUseBlock // 工具调用块
assistantMessage: AssistantMessage // 所属的 assistant 消息
status: ToolStatus // 'pending' | 'executing' | 'done' | 'error'
isConcurrencySafe: boolean // 并发安全标记
promise?: Promise<void> // 执行 Promise
results?: Message[] // 执行结果
pendingProgress: Message[] // 进度消息缓冲
contextModifiers?: Array< // 上下文修改器
(context: ToolUseContext) => ToolUseContext
>
}
5.2 并发策略:canExecuteTool()
typescript
// src/services/tools/StreamingToolExecutor.ts:129-135
private canExecuteTool(isConcurrencySafe: boolean): boolean {
// 当前执行中的工具,支持并发的工具,即使执行中的工具不为0,也返回true;串行时仅当前执行中的工具为0时返回true;
const executingTools = this.tools.filter(t => t.status === 'executing')
return (
executingTools.length === 0 ||
(isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe))
)
}
// 此函数会在每次executeTool结束后调用,直到队列中所有tools都不为`queued`
private async processQueue(): Promise<void> {
for (const tool of this.tools) {
// 如果当前工具非排队,直接轮询
if (tool.status !== 'queued') continue
if (this.canExecuteTool(tool.isConcurrencySafe)) {
// 执行工具
await this.executeTool(tool)
} else {
if (!tool.isConcurrencySafe) break
}
}
}
private async executeTool(tool: TrackedTool): Promise<void> {
// ...
const promise = collectResults()
tool.promise = promise
// 每个工具执行结束都再次调用`processQueue`
void promise.finally(() => {
void this.processQueue()
})
}
Claude Code的并发设计并不复杂,支持并行的工具会在第一次执行的时候全部推入processQueue执行队列,不支持并行的工具会在最后一个并行工具执行完后再推入processQueue队列,直至所有工具执行完。
- 典型的并发安全工具:
GlobTool、GrepTool、FileReadTool、WebSearchTool - 典型的非并发安全工具:
BashTool、FileEditTool、FileWriteTool
5.3 有序结果产出
即使工具并发执行,结果仍然按原始 tool_use 块的顺序产出:
typescript
// StreamingToolExecutor 的 processQueue 逻辑
// tools 数组维持原始顺序
// 每个 tool 执行完成后检查是否可以产出结果
// 只有前序工具都完成后,当前工具的结果才会被 yield
这保证了对话流中的消息顺序与模型生成的工具调用顺序一致。
5.4 兄弟中止机制
typescript
// src/services/tools/StreamingToolExecutor.ts:48
// siblingAbortController: 当一个 Bash 工具出错时,中止所有兄弟工具
private getAbortReason(tool: TrackedTool): string {
// 检查中止原因:
// - 用户中断 (user_interrupted)
// - 兄弟工具错误 (sibling_error)
// - 流式回退 (streaming_fallback)
}
当同一批次中的一个 BashTool 执行失败时,其他尚未完成的工具会被中止。中止原因被记录到 createSyntheticErrorMessage() 中,产出一个合成的错误消息告知模型。
typescript
// src/services/tools/StreamingToolExecutor.ts:153
private createSyntheticErrorMessage(
toolUseId: string,
reason: 'sibling_error' | 'user_interrupted' | 'streaming_fallback',
assistantMessage: AssistantMessage,
): Message {
// 生成描述性错误消息,让模型理解为什么工具被取消
}
六、MCP 系统
源码位置:
src/services/mcp/client.ts、src/tools/MCPTool/MCPTool.ts
MCP (Model Context Protocol) 是 Anthropic 定义的标准协议,允许外部服务向 Claude 提供工具、资源和提示。Claude Code 实现了完整的 MCP 客户端。
6.1 四种传输协议
typescript
// src/services/mcp/client.ts
// 支持四种 MCP 传输方式
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
// WebSocketTransport 也被支持
import { WebSocketTransport } from '../../utils/mcpWebSocketTransport.js'
| 传输方式 | 适用场景 | 配置方式 |
|---|---|---|
| stdio | 本地进程 | command + args |
| SSE | HTTP 长连接 | url (带 /sse 后缀) |
| StreamableHTTP | HTTP 流式 | url (非 /sse) |
| WebSocket | 全双工 | url (ws:// 或 wss://) |
6.2 MCPTool 模板 --- 工具包装
MCP工具也是通过buildTool进行包装,方便实现
typescript
// src/tools/MCPTool/MCPTool.ts:27-77
export const MCPTool = buildTool({
isMcp: true,
// 以下属性在 mcpClient.ts 中被运行时覆盖
name: 'mcp', // → mcp__<server>__<tool>
async description() { return DESCRIPTION }, // → MCP 服务端描述
async prompt() { return PROMPT },
get inputSchema() {
return inputSchema() // z.object({}).passthrough()
},
async call() { return { data: '' } }, // → 实际 MCP 调用
async checkPermissions(): Promise<PermissionResult> {
return { behavior: 'passthrough', message: 'MCPTool requires permission.' }
},
maxResultSizeChars: 100_000,
userFacingName: () => 'mcp', // → 实际工具名
//...
})
MCPTool 本身是一个空壳模板。关键属性(name、description、call、inputSchema)在 mcpClient.ts 的 connectToServer() 中被运行时覆盖。
6.3 工具命名约定
typescript
// src/services/mcp/mcpStringUtils.ts:39
export function getMcpPrefix(serverName: string): string {
return `mcp__${normalizeNameForMCP(serverName)}__`
}
export function buildMcpToolName(serverName: string, toolName: string): string {
return `${getMcpPrefix(serverName)}${normalizeNameForMCP(toolName)}`
}
命名格式:mcp__<serverName>__<toolName>
例如:
mcp__ide__getDiagnostics--- IDE MCP Server 的诊断工具mcp__filesystem__readFile--- 文件系统 MCP Server 的读文件工具
这种命名方式使得 filterToolsByDenyRules() 可以通过前缀 mcp__ide 一次性屏蔽整个 Server 的所有工具。
6.4 连接管理
(useManageMCPConnections.ts:894)"] --> B["connectToServer
(mcp/client.ts:596)"] B --> C["onConnectionAttempt
更新 appState
(useManageMCPConnections.ts:310)"] C --> D["useMergedTools
(REPL.tsx:1034)"] D --> E["assembleToolPool
合并工具"]
当Claude Code启动后就会进行MCP的连接,连接成功后通过更新appState使REPL进行工具合并。
核心连接逻辑在connectToServer
typescript
// src/services/mcp/client.ts:596
// memoize 确保同一个 server 只建立一次连接
export const connectToServer = memoize(async function connectToServer(
serverConfig: MCPServerConfig,
// ...
): Promise<MCPServerConnection> {
// 1. 选择传输方式
// 2. 建立连接
// 3. 获取工具列表
// 4. 为每个工具创建 MCPTool 实例(覆盖模板属性)
// 5. 返回连接对象
})
关键常量:
DEFAULT_MCP_TOOL_TIMEOUT_MS = 100_000_000(约 27.8 小时)--- MCP 工具默认超时MAX_MCP_DESCRIPTION_LENGTH = 2048--- 描述截断阈值
6.5 MCP 工具与内置工具的合并
MCP 工具在 assembleToolPool() 中与内置工具合并(见 3.3 节)。
这意味着如果一个 MCP 工具的名称与内置工具冲突,内置工具会胜出。
七、Command 命令系统
源码位置:
src/types/command.ts、src/commands.ts、src/utils/processUserInput/processSlashCommand.tsx
Command(命令)是 Claude Code 的用户交互入口,用户通过在终端输入 /command 触发各种操作。Command 系统独立于 Tool 系统------Tool 由模型调用,而 Command 由用户直接调用。两者通过 SkillTool 产生交集:Command既可通过 / 触发,也可被模型通过 SkillTool 调用。我们经常说的Skill也是Command的一种实现。
7.1 Command 类型体系
typescript
// src/types/command.ts:205
export type Command = CommandBase &
(PromptCommand | LocalCommand | LocalJSXCommand)
Command 由公共基类 CommandBase 和三种具体类型的联合组成。
CommandBase --- 公共属性
typescript
// src/types/command.ts:175-203
export type CommandBase = {
availability?: CommandAvailability[] // 可用环境('claude-ai' | 'console')
description: string // 命令描述
isEnabled?: () => boolean // 运行时启用条件(默认 true)
isHidden?: boolean // 是否从补全/帮助中隐藏
name: string // 唯一标识(如 'clear'、'config')
aliases?: string[] // 别名(如 clear 的别名 ['reset', 'new'])
whenToUse?: string // 模型调用时机描述
disableModelInvocation?: boolean // 是否禁止模型调用
userInvocable?: boolean // 用户是否可通过 / 触发
loadedFrom?: LoadedFrom // 来源标记
immediate?: boolean // 是否立即执行(不等待队列)
// ...
}
LoadedFrom --- 来源标记
typescript
// src/skills/loadSkillsDir.ts:67-74
type LoadedFrom =
| 'commands_DEPRECATED' // 旧版 .claude/commands/
| 'skills' // .claude/skills/
| 'plugin' // 插件目录
| 'managed' // managed skills (系统管理)
| 'bundled' // 内置打包
| 'mcp' // MCP 协议提供
三种命令类型
| 类型 | 触发方式 | 返回值 | 典型示例 |
|---|---|---|---|
LocalCommand |
用户 /command |
文本结果 | /clear、/compact、/files |
LocalJSXCommand |
用户 /command |
React 组件 | /config、/status、/help |
PromptCommand |
用户 /skill 或模型 SkillTool |
注入对话流 | 用户定义的 .md 技能 |
LocalCommand --- 纯文本命令:
typescript
// src/types/command.ts:74-78
type LocalCommand = {
type: 'local'
supportsNonInteractive: boolean // 是否支持非交互模式
load: () => Promise<LocalCommandModule> // 懒加载实现
}
// LocalCommandModule 接口
type LocalCommandModule = {
call: (args: string, context: LocalJSXCommandContext) => Promise<LocalCommandResult>
}
// 返回值三种形态
type LocalCommandResult =
| { type: 'text'; value: string } // 普通文本输出
| { type: 'compact'; compactionResult: CompactionResult } // 压缩操作
| { type: 'skip' } // 静默执行
LocalJSXCommand --- UI 渲染命令:
typescript
// src/types/command.ts:144
type LocalJSXCommand = {
type: 'local-jsx'
load: () => Promise<LocalJSXCommandModule>
}
// 调用签名
type LocalJSXCommandCall = (
onDone: LocalJSXCommandOnDone, // 完成回调
context: ToolUseContext & LocalJSXCommandContext,
args: string,
) => Promise<React.ReactNode> // 返回 JSX 渲染到终端
onDone 回调控制命令完成后的行为:
typescript
// src/types/command.ts:117-126
type LocalJSXCommandOnDone = (
result?: string,
options?: {
display?: 'skip' | 'system' | 'user' // 结果展示方式
shouldQuery?: boolean // 是否继续查询模型
metaMessages?: string[] // 注入隐藏消息
nextInput?: string // 下一轮自动输入
submitNextInput?: boolean // 是否自动提交
},
) => void
PromptCommand --- 提示词命令(即 Skill):
typescript
// src/types/command.ts:25-57
type PromptCommand = {
type: 'prompt'
progressMessage: string // 加载时的进度消息
contentLength: number // 内容长度(用于 token 估算)
argNames?: string[] // 参数名列表
allowedTools?: string[] // 限制可用工具
model?: string // 指定模型
context?: 'inline' | 'fork' // 执行上下文 inline为当前对话执行 fork为子agent里执行
agent?: string // fork 时使用的 agent 类型
effort?: EffortValue // 推理深度
paths?: string[] // 条件触发的文件路径 glob
getPromptForCommand( // 生成 prompt 内容
args: string,
context: ToolUseContext,
): Promise<ContentBlockParam[]>
}
7.2 命令注册机制
所有内置命令通过 COMMANDS() 工厂函数注册,采用 memoize 确保只初始化一次:
typescript
// src/commands.ts:258
const COMMANDS = memoize((): Command[] => [
addDir,
agents,
branch,
clear, // type: 'local'
compact, // type: 'local'
config, // type: 'local-jsx'
help, // type: 'local-jsx'
status, // type: 'local-jsx'
// ... 更多内置命令
// 条件加载
...(webCmd ? [webCmd] : []),
...(voiceCommand ? [voiceCommand] : []),
...(process.env.USER_TYPE === 'ant' ? INTERNAL_ONLY_COMMANDS : []),
])
每个内置命令是一个简洁的描述符对象,实现代码通过 load() 懒加载:
typescript
// src/commands/clear/index.ts
const clear = {
type: 'local',
name: 'clear',
description: 'Clear conversation history and free up context',
aliases: ['reset', 'new'],
supportsNonInteractive: false,
load: () => import('./clear.js'), // 懒加载
} satisfies Command
// src/commands/config/index.ts
const config = {
aliases: ['settings'],
type: 'local-jsx',
name: 'config',
description: 'Open config panel',
load: () => import('./config.js'),
} satisfies Command
7.3 命令发现:getCommands()
getCommands() 是最终的命令列表组装函数,合并来自多个来源的命令:
typescript
// src/commands.ts:451-471
const loadAllCommands = memoize(async (cwd: string): Promise<Command[]> => {
const [
{ skillDirCommands, pluginSkills, bundledSkills, builtinPluginSkills },
pluginCommands,
workflowCommands,
] = await Promise.all([
getSkills(cwd), // Skill 目录
getPluginCommands(), // 插件命令
getWorkflowCommands?.(cwd) ?? Promise.resolve([]), // 工作流命令
])
return [
...bundledSkills, // 内置打包技能
...builtinPluginSkills, // 内置插件技能
...skillDirCommands, // 技能目录
...workflowCommands, // 工作流
...pluginCommands, // 插件
...pluginSkills, // 插件技能
...COMMANDS(), // 内置命令(最后)
]
})
最终过滤和动态技能合并:
typescript
// src/commands.ts:478-519
export async function getCommands(cwd: string): Promise<Command[]> {
const allCommands = await loadAllCommands(cwd)
const dynamicSkills = getDynamicSkills() // 运行时动态发现的技能
// 过滤:可用性 + 启用状态
const baseCommands = allCommands.filter(
_ => meetsAvailabilityRequirement(_) && isCommandEnabled(_),
)
// 动态技能去重后插入到内置命令之前
const builtInNames = new Set(COMMANDS().map(c => c.name))
const insertIndex = baseCommands.findIndex(c => builtInNames.has(c.name))
return [
...baseCommands.slice(0, insertIndex),
...uniqueDynamicSkills,
...baseCommands.slice(insertIndex),
]
}
合并优先级(先出现的同名命令胜出):bundledSkills > builtinPluginSkills > skillDirCommands > workflowCommands > pluginCommands > pluginSkills > COMMANDS()。
7.4 斜杠命令执行流程
用户输入 /command args 后的完整处理链路:
普通对话"] D --> F["parseSlashCommand()
解析命令名和参数"] F --> G{"找到命令?"} G -->|否| H["返回 Unknown skill 错误"] G -->|是| I["getMessagesForSlashCommand()"] I --> J{"command.type"} J -->|local| K["command.load().call()
同步执行,返回文本"] J -->|local-jsx| L["command.load().call(onDone)
渲染 JSX,等待 onDone"] J -->|prompt| M{"context === 'fork'?"} M -->|是| N["executeForkedSlashCommand()
子 Agent 执行"] M -->|否| O["getMessagesForPromptSlashCommand()
展开 prompt 注入对话"]
local 类型的执行
typescript
// src/utils/processUserInput/processSlashCommand.tsx:860-949
case 'local': {
const mod = await command.load()
const result = await mod.call(args, context)
if (result.type === 'skip') {
return { messages: [], shouldQuery: false, command }
}
// 结果包装为 <local-command-stdout> 标签
return {
messages: [
userMessage,
createCommandInputMessage(
`<local-command-stdout>${result.value}</local-command-stdout>`,
),
],
shouldQuery: false, // local 命令不触发模型查询
command,
}
}
local-jsx 类型的执行
typescript
// src/utils/processUserInput/processSlashCommand.tsx:732-859
case 'local-jsx': {
return new Promise<SlashCommandResult>(resolve => {
const onDone: LocalJSXCommandOnDone = (result, options) => {
// 根据 display 选项决定结果展示方式
// 'skip' → 无消息
// 'system' → 系统消息
// 'user' → 用户消息
resolve({ messages, shouldQuery: options?.shouldQuery ?? false, command })
}
// 懒加载并执行,返回 JSX 渲染到终端
command.load()
.then(mod => mod.call(onDone, { ...context, canUseTool }, args))
.then(jsx => {
setToolJSX({
jsx,
shouldHidePromptInput: true, // 隐藏输入框
showSpinner: false,
isLocalJSXCommand: true,
})
})
})
}
prompt 类型的展开
typescript
// src/utils/processUserInput/processSlashCommand.tsx:1114-1262
async function getMessagesForPromptSlashCommand(command, args, context) {
// 1. 调用 getPromptForCommand 生成内容
const result = await command.getPromptForCommand(args, context)
// 2. 构造元信息
const metadata = formatCommandLoadingMetadata(command, args)
// 3. 解析允许的工具列表
const additionalAllowedTools = parseToolListFromCLI(command.allowedTools ?? [])
// 4. 组装消息序列
const messages = [
createUserMessage({ content: metadata, uuid }), // 元数据
createUserMessage({ content: result, isMeta: true }), // 技能内容(隐藏)
...attachmentMessages, // 附件
createAttachmentMessage({ // 权限声明
type: 'command_permissions',
allowedTools: additionalAllowedTools,
model: command.model,
}),
]
return {
messages,
shouldQuery: true, // prompt 命令触发模型查询
allowedTools: additionalAllowedTools,
model: command.model,
effort: command.effort,
command,
}
}
与 local/local-jsx 不同,prompt 类型的 shouldQuery 为 true------内容注入对话流后会触发模型响应。
八、Skill 技能系统
源码位置:
src/skills/loadSkillsDir.ts
Skill(技能)是 PromptCommand 的具体实现形式,允许用户通过 Markdown 文件定义可复用的 prompt 模板。Skill 是 Command 系统与 Tool 系统的桥梁------它以 Command 的身份被用户 / 调用,也可以通过 SkillTool 被模型主动调用。
8.1 Skill 加载流程
~/.claude/skills/managed/"] B --> D["User Skills
~/.claude/skills/"] B --> E["Project Skills
.claude/skills/ (各层)"] B --> F["Additional Skills
additionalSkillPaths"] B --> G["Legacy Skills
.claude/commands/ (已废弃)"] C --> H["parseSkillFile()"] D --> H E --> H F --> H G --> H H --> I["解析 Frontmatter"] I --> J["createSkillCommand()"] J --> K["去重 + 分离条件技能"]
getSkillsPath() --- 路径解析
typescript
// src/skills/loadSkillsDir.ts:78-94
export function getSkillsPath(
source: SettingSource | 'plugin',
dir: 'skills' | 'commands',
): string {
switch (source) {
case 'policySettings':
return join(getManagedFilePath(), '.claude', dir)
case 'userSettings':
return join(getClaudeConfigHomeDir(), dir)
case 'projectSettings':
return `.claude/${dir}`
case 'plugin':
return 'plugin'
default:
return ''
}
}
8.2 Skill 文件格式 (Frontmatter)
一个完整的 Skill Markdown 文件:
markdown
---
name: Review PR
description: Review a pull request for code quality
allowed-tools: Bash, FileReadTool, GrepTool
arguments: pr_number
when_to_use: when the user asks to review a PR
model: opus
effort: high
context: fork
userInvocable: true
disable-model-invocation: false
---
Review the pull request #$ARGUMENTS and provide feedback on:
1. Code quality
2. Potential bugs
3. Performance issues
解析源码参考parseSkillFrontmatterFields(src/skills/loadSkillsDir.ts:185)
| 字段 | 类型 | 说明 |
|---|---|---|
name |
string | UI 显示名 |
description |
string | 技能描述 |
allowed-tools |
string[] | 限制可用工具 |
arguments |
string[] | 参数名列表 |
when_to_use |
string | 模型自动调用的触发条件 |
model |
string | 指定使用的模型 |
effort |
string | 推理深度 |
context |
'fork' | 在子 agent 中执行 |
userInvocable |
boolean | 是否可通过 / 触发 |
disable-model-invocation |
boolean | 禁止模型自动调用 |
shell |
string | Bash 工具使用的 shell |
8.3 变量替换
Skill 内容支持以下变量:
$ARGUMENTS--- 用户传入的参数${CLAUDE_SKILL_DIR}- skills存放路径${CLAUDE_SESSION_ID}- 当前sessionId
相关处理在src/skills/loadSkillsDir.ts: 270中,函数为createSkillCommand
8.4 条件技能 (Conditional Skills)
通过 paths 字段实现文件路径匹配的条件技能:
typescript
// src/types/command.ts:50-52
export type PromptCommand = {
// ...
paths?: string[]
// ...
}
条件技能只在模型操作过匹配路径的文件后才变为可见。例如,配置 paths: ["*.test.ts"] 的测试技能只有在模型读取或编辑了测试文件后才会出现在可用技能列表中。项目级技能(projectSettings 来源)天然隔离于项目目录内。
九、SkillTool ------ 模型驱动的技能调用
源码位置:
src/tools/SkillTool/SkillTool.ts
SkillTool 是一个特殊的内置工具,它让模型可以主动调用用户定义的 Skill,而不仅仅是通过用户输入 / 命令触发。
9.1 工作机制
9.2 getAllCommands() --- 命令发现
typescript
// src/tools/SkillTool/SkillTool.ts:81-94
async function getAllCommands(context: ToolUseContext): Promise<Command[]> {
// 1. 获取 MCP 提供的技能
const mcpSkills = context.getAppState().mcp.commands.filter(
cmd => cmd.type === 'prompt' && cmd.loadedFrom === 'mcp',
)
// 2. 没有 MCP 技能时直接返回本地命令
if (mcpSkills.length === 0) {
return getCommands(getProjectRoot())
}
// 3. 合并本地命令和 MCP 技能,本地优先
const localCommands = await getCommands(getProjectRoot())
return uniqBy([...localCommands, ...mcpSkills], 'name')
}
合并顺序是 localCommands 在前,mcpSkills 在后。uniqBy 保留第一个,所以本地命令优先于同名 MCP 技能。
9.3 validateInput --- 技能存在性校验
SkillTool 的 validateInput 确保:
- 请求的技能名称存在于可用命令列表中
- 目标命令是
PromptCommand类型(非 local/local-jsx) - 技能没有设置
disableModelInvocation: true
如果校验失败,返回描述性错误消息,模型可以据此调整调用。
9.4 executeForkedSkill() --- 子 Agent 执行
当 Skill 的 frontmatter 设置 executionContext: 'fork' 时,技能在隔离的子 Agent 中运行:
typescript
// src/tools/SkillTool/SkillTool.ts:122-200+
async function executeForkedSkill(
skillName: string,
prompt: string,
context: ToolUseContext,
allowedTools?: string[],
model?: string,
effort?: string,
): Promise<ToolResult<string>> {
// 1. 构造子 agent 的配置
// 2. 通过 runAgent() 在独立上下文中执行
// 3. 跟踪分析事件
// 4. 返回子 agent 的结果
}
Fork 执行的优势:
- 隔离性:子 Agent 有独立的上下文,不污染主对话
- 工具限制 :可以通过
allowedTools限制子 Agent 可用的工具 - 模型选择:可以为特定技能指定不同的模型
9.5 inline 整合回主会话
inline模式调用getMessagesForPromptSlashCommand,参考7.4节
十、系统集成场景
10.1 端到端流程:模型调用内置工具
10.2 端到端流程:MCP 工具调用
10.3 端到端流程:Skill 调用
十一、设计洞察
11.1 统一接口的力量
所有工具------无论是核心的 BashTool、外部的 MCP 工具、还是用户定义的 Skill------都实现同一个 Tool<Input, Output, P> 接口。这意味着:
- 权限系统只需实现一次,自动应用于所有工具
StreamingToolExecutor的并发控制对所有工具类型透明- 新增工具类型无需修改执行管道
10.2 多层权限的纵深防御
权限检查不是一个单点决策,而是一条贯穿整个执行管道的防线:
markdown
配置文件 deny 规则 → filterToolsByDenyRules(注册时过滤)
→ Zod schema 校验 → validateInput 业务校验
→ PreToolUse hooks(用户自定义拦截)
→ resolveHookPermissionDecision(综合决策)
→ canUseTool(运行时权限)
→ 内部字段剥离(防注入)
即使某一层被绕过,后续层仍然提供保护。
10.3 并发安全的简洁模型
用一个布尔值 isConcurrencySafe 就实现了完整的并发控制:
- 全部安全 → 全并发(如多个
GlobTool查询) - 任一不安全 → 排队执行(如
FileEditTool必须独占) - 兄弟中止 → Bash 失败时取消同批次工具
这比复杂的锁机制更易理解、更少出错。
10.4 Skill 系统的渐进式复杂度
Skill 系统展现了优秀的渐进式设计:
- 最简形式:一个 Markdown 文件,内容即 prompt → 零配置
- 中等复杂:添加 frontmatter 控制工具、模型、参数 → 声明式配置
- 高级用法 :
executionContext: fork在子 Agent 中运行 → 完全隔离
用户可以从最简单的形式开始,按需增加复杂度。