Claude Code 源码深度解析

第2章 架构总览

作者 杨艺韬 · 13,708 字 · 发布于 · 更新于

"The goal of software architecture is to minimize the human resources required to build and maintain the required system." -- Robert C. Martin, Clean Architecture(2017)

本章要点

  • 一条消息的完整旅程:从用户在终端输入,到最终响应输出,中间经历了入口解析、消息标准化、系统提示词组装、API 流式调用、工具调用、权限检查等十余个环节
  • 六大核心子系统:CLI 入口与启动系统、Query 引擎、工具系统、权限系统、MCP 集成、Remote Control 桥接(src/bridge/),各自承担明确的职责边界
  • Generator 驱动的流式管道:整个 query 循环基于 async function* 实现,数据以 yield 方式逐块流出,天然支持流式渲染和中断恢复
  • 工具即自描述对象:每个工具是一个实现了统一 Tool 接口的对象,包含 schema 定义、权限检查、执行逻辑、并发安全性声明等元信息
  • 特性标志与死代码消除:通过 Bun 的 feature() 宏在编译期裁剪代码分支,外部构建产物不包含内部实验性功能

2.1 跟踪一条消息的完整旅程

理解一个复杂系统最好的方式,不是先看它的类图或模块划分,而是跟踪一个具体的数据流从头到尾走一遍。就像理解一座城市的交通系统,最好的方法是坐一趟公交车走完全程,而不是先研究线路图。

让我们假设用户在终端输入了一句话:

> 帮我写一个快速排序函数

这条消息从键入到最终排序函数出现在屏幕上,会经历怎样的旅程?

第一站:CLI 入口接收输入

一切始于 src/entrypoints/cli.tsx。这是 Claude Code 的真正入口点。它的设计有一个精妙之处——为高频快速路径做了零依赖优化:

// src/entrypoints/cli.tsx
async function main(): Promise<void> {
  const args = process.argv.slice(2);

  // Fast-path for --version/-v: zero module loading needed
  if (args.length === 1 &&
      (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) {
    console.log(`${MACRO.VERSION} (Claude Code)`);
    return;
  }
  // ...后续才开始加载完整的启动模块
}

当没有命中快速路径时,控制权转移到 src/main.tsx——整个应用的主编排器。main.tsx 的开头就展示了启动性能的极致追求:

// src/main.tsx (前20行)
import { profileCheckpoint } from './utils/startupProfiler.js';
profileCheckpoint('main_tsx_entry');

import { startMdmRawRead } from './utils/settings/mdm/rawRead.js';
startMdmRawRead(); // 启动 MDM 子进程,与后续 135ms 的 import 并行

import { startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js';
startKeychainPrefetch(); // 并行预取 macOS keychain 中的 OAuth 和 API key

这三条 import 被故意放在最前面,并且每条都立即执行一个副作用函数。注释给出的意图是:MDM(Mobile Device Management)配置读取和 Keychain 凭证预取是耗时的 I/O 操作,尽早发起、与后续启动工作并行,就不再占用启动关键路径——按源码注释,仅 macOS 上两次 Keychain 读取顺序执行就要约 65ms。不过 ES 模块的 import 会提升,依赖先全部求值,这两个调用实际要等 import 链走完才执行,真正与之重叠的是 main() 的前段,而不是约 135ms 的模块加载(细节见第 3 章 §3.1.2、§3.2)。

在交互模式下,main.tsx 经过一系列初始化步骤后,最终调用 launchRepl() 函数启动 React/Ink 终端 UI。Ink 是一个将 React 组件渲染到终端的库,Claude Code 在 src/ink/ 下维护了一套深度定制的实现(第 17 章),用它构建了完整的终端界面——包括消息展示、进度指示、权限确认对话框等。REPL 组件(src/screens/REPL.tsx)接收到用户输入后,直接调用 query() 并逐个消费它产出的事件(第 5 章 §5.3.2);QueryEngine 的 submitMessage() 则是 SDK 与 -p 无头模式的入口(第 4 章 §4.1)。两条路径最终汇入同一个 query()。

main.tsx 的初始化同样尽量不让用户干等:Commander 的 preAction 钩子先收割 MDM / Keychain 预取的结果,再执行 init() 和配置迁移;远程托管设置、策略限制以 fire-and-forget 方式在后台加载;用户信息、Git 状态等为第一次查询准备的预取,则推迟到首帧渲染之后(第 3 章 §3.5.5)。

第二站:消息标准化

用户的文本字符串 "帮我写一个快速排序函数" 不能直接发送给 API。它需要被包装成 Claude API 能理解的消息格式。这个标准化过程发生在 src/utils/messages.ts:

// src/utils/messages.ts
import { randomUUID } from 'crypto';

// 创建一条用户消息
export function createUserMessage({
  content,
  toolUseResult,
  sourceToolAssistantUUID,
}: {
  content: ContentBlockParam[];
  toolUseResult?: string;
  sourceToolAssistantUUID?: string;
}) {
  return {
    type: 'user' as const,
    uuid: randomUUID(),
    message: { role: 'user' as const, content },
    toolUseResult,
    sourceToolAssistantUUID,
  };
}

每条消息都被赋予一个 UUID。这个 UUID 在后续的工具调用链中起到关键的追踪作用——当一个工具产生的结果需要关联回触发它的助手消息时,sourceToolAssistantUUID 就是连接它们的纽带。

第三站:系统提示词组装

在消息发送到 API 之前,需要构建系统提示词。这不是一个简单的字符串拼接,而是一个涉及多个数据源的聚合过程。src/utils/queryContext.ts 中的 fetchSystemPromptParts() 函数负责此工作:

// src/utils/queryContext.ts
export async function fetchSystemPromptParts({
  tools, mainLoopModel, additionalWorkingDirectories,
  mcpClients, customSystemPrompt,
}: { ... }): Promise<{
  defaultSystemPrompt: string[];
  userContext: { [k: string]: string };
  systemContext: { [k: string]: string };
}> {
  const [defaultSystemPrompt, userContext, systemContext] =
    await Promise.all([
      customSystemPrompt !== undefined
        ? Promise.resolve([])
        : getSystemPrompt(tools, mainLoopModel,
            additionalWorkingDirectories, mcpClients),
      getUserContext(),
      customSystemPrompt !== undefined
        ? Promise.resolve({})
        : getSystemContext(),
    ]);
  return { defaultSystemPrompt, userContext, systemContext };
}

系统提示词由三部分组成:

  1. 默认系统提示词(getSystemPrompt):包含 Claude Code 的核心行为指令,由 src/constants/prompts.ts 构建。它会动态感知当前可用的工具列表、操作系统类型、Git 状态等环境信息。

  2. 用户上下文(getUserContext):CLAUDE.md 记忆文件的内容(claudeMd)与当前日期(currentDate)。由 src/context.ts 中 getClaudeMds() 等函数收集。

  3. 系统上下文(getSystemContext):一份 Git 状态快照(当前分支、主分支、git status、最近提交等),在 2.1.89 的 cli.js 里它只有 gitStatus 这一个键;不在 Git 仓库等情况下为空。工作目录、操作系统这类环境信息属于默认系统提示词。

这三者通过 Promise.all 并行获取——又一个性能优化的细节。注意函数签名中的 customSystemPrompt 参数:当调用方提供了自定义系统提示词时(SDK 选项,或 --system-prompt 命令行参数),默认的系统提示词和系统上下文都会被跳过。这个设计让 SDK 调用者可以完全控制模型看到的提示词,而不是在默认提示词上叠加。

系统提示词的构建由 src/constants/prompts.ts 中的 getSystemPrompt() 函数完成。它会根据当前可用的工具列表动态生成工具使用说明,根据操作系统类型调整文件路径格式,内置的 Git 提交 / PR 流程指令则由 includeGitInstructions 设置(或环境变量 CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS)控制,默认开启,与是否处于 Git 仓库无关(是否在 Git 仓库只影响系统上下文里带不带 git 状态快照)。这不是一个静态的模板,而是一个对运行时环境高度敏感的动态构建过程。

第四站:进入 Query 循环

组装完成后,控制权进入 src/query.ts 中的 query() 函数。这是整个系统最核心的函数之一。它是一个 async function*(异步 Generator),这个选择不是偶然的——Generator 天然适合表达"产生一系列结果,中间可以暂停和恢复"的语义:

// src/query.ts
export async function* query(
  params: QueryParams,
): AsyncGenerator<
  StreamEvent | RequestStartEvent | Message | TombstoneMessage
    | ToolUseSummaryMessage,
  Terminal
> {
  const consumedCommandUuids: string[] = [];
  const terminal = yield* queryLoop(params, consumedCommandUuids);
  for (const uuid of consumedCommandUuids) {
    notifyCommandLifecycle(uuid, 'completed');
  }
  return terminal;
}

queryLoop() 是一个 while(true) 无限循环。每次迭代代表一个"轮次"(turn):发送消息到 API、接收响应、如果响应包含工具调用则执行工具并继续循环;如果响应是纯文本则退出。这种"循环直到模型不再调用工具"的模式是 Agent 系统的经典范式——它让模型可以自主决定需要执行多少步操作才能完成任务,而不是由外部预先指定步骤数。

在循环开始之前,queryLoop 会构建一些重要的辅助设施:通过 buildQueryConfig() 快照一次性的环境配置(避免在循环内重复读取会变化的状态),通过 startRelevantMemoryPrefetch() 预取可能相关的记忆文件(在模型还在生成时就开始准备下一轮可能需要的上下文)。

QueryParams 的定义清楚地展示了一次 query 需要的所有输入:

// src/query.ts
export type QueryParams = {
  messages: Message[];
  systemPrompt: SystemPrompt;
  userContext: { [k: string]: string };
  systemContext: { [k: string]: string };
  canUseTool: CanUseToolFn;
  toolUseContext: ToolUseContext;
  fallbackModel?: string;
  querySource: QuerySource;
  maxTurns?: number;
  taskBudget?: { total: number };
  // ...
};

第五站:API 调用与流式响应

在 queryLoop 的每次迭代中,消息被发送到 Claude API。调用发生在 src/services/api/claude.ts 中。调用之前,消息经过关键的预处理:

// src/query.ts 中的调用
for await (const message of deps.callModel({
  messages: prependUserContext(messagesForQuery, userContext),
  systemPrompt: fullSystemPrompt,
  thinkingConfig: toolUseContext.options.thinkingConfig,
  tools: toolUseContext.options.tools,
  signal: toolUseContext.abortController.signal,
  options: {
    model: currentModel,
    querySource,
    // ...
  },
})) {
  // 处理每个流式事件...
}

prependUserContext 将用户上下文(如 CLAUDE.md 内容)注入到消息序列前部,利用 API 的前缀缓存机制——相同前缀的消息在后续请求中可以命中缓存,降低输入 Token 的计费与延迟(Token 数本身不变)。AbortController 的 signal 被传入,使得用户按 Esc 时可以立即中断 API 调用。这种基于标准 Web API 的取消机制确保了从网络请求到工具执行的整个链路都能被即时中断。

在实际调用 API 之前,query 循环还会依次执行几个上下文管理步骤:应用工具结果预算(applyToolResultBudget,将超出预算的工具结果替换为摘要占位符)、裁剪压缩(snipCompact,不调用 API、零成本地移除部分低价值消息)、微压缩(microcompact,清理旧的工具结果)、上下文折叠(context collapse,只改变发给 API 的"视图")、自动压缩(autocompact,在 Token 数接近上限时触发完整的上下文总结)。顺序与各步细节见第 4 章 §4.2.2 与第 16 章。这些机制确保长对话不会因为上下文溢出而崩溃,同时尽可能保留对当前任务有用的信息。

API 返回的是一个流式响应。每当收到一个 assistant 类型的消息块时,系统检查其中是否包含 tool_use 类型的内容块:

if (message.type === 'assistant') {
  assistantMessages.push(message);
  const msgToolUseBlocks = message.message.content.filter(
    content => content.type === 'tool_use',
  ) as ToolUseBlock[];
  if (msgToolUseBlocks.length > 0) {
    toolUseBlocks.push(...msgToolUseBlocks);
    needsFollowUp = true; // 标记需要继续循环
  }
}

对于我们的排序函数请求,如果模型决定直接用文本回复(内联给出排序代码),响应会被 yield 出去并最终渲染到终端。但如果模型决定先创建一个文件再写入代码(这在实际使用中更常见,因为代码通常需要保存到文件中才有用),它会返回一个包含 tool_use 块的响应。例如,模型可能会返回一个调用 FileWrite 工具(源码里是 FileWriteTool,对模型暴露的名字是 Write)的请求,参数是文件路径 sort.py 和文件内容(排序函数的代码)。

第六站:工具调用与权限检查

当检测到工具调用时,系统进入工具执行阶段。src/services/tools/toolOrchestration.ts 中的 runTools() 负责编排:

// src/services/tools/toolOrchestration.ts(简化,省略上下文修改器的处理,完整版见第 7 章)
export async function* runTools(
  toolUseMessages: ToolUseBlock[],
  assistantMessages: AssistantMessage[],
  canUseTool: CanUseToolFn,
  toolUseContext: ToolUseContext,
): AsyncGenerator<MessageUpdate, void> {
  let currentContext = toolUseContext;
  for (const { isConcurrencySafe, blocks } of
    partitionToolCalls(toolUseMessages, currentContext)) {
    if (isConcurrencySafe) {
      // 并发安全的工具可以并发执行
      for await (const update of runToolsConcurrently(
        blocks, assistantMessages, canUseTool, currentContext
      )) { yield { message: update.message, newContext: currentContext }; }
    } else {
      // 非并发安全的工具必须串行执行
      for await (const update of runToolsSerially(
        blocks, assistantMessages, canUseTool, currentContext
      )) { /* ... */ }
    }
  }
}

这里有一个重要的设计决策:工具调用被分为"并发安全"和"非并发安全"两类。读取文件、搜索代码等只读操作可以并行执行以提高效率;而写入文件、执行 shell 命令等有副作用的操作必须串行执行以保证正确性。每个工具通过 isConcurrencySafe() 方法自声明其并发安全性。

在工具真正执行之前,必须通过权限检查。canUseTool 作为参数一路传进 runTools(),由单个工具的执行流程(第七站的 runToolUse())在 PreToolUse 钩子之后调用;交互模式下它由 src/hooks/useCanUseTool.tsx 中的 Hook 提供,是权限系统的入口:

// src/hooks/useCanUseTool.tsx
export type CanUseToolFn = (
  tool: ToolType,
  input: Input,
  toolUseContext: ToolUseContext,
  assistantMessage: AssistantMessage,
  toolUseID: string,
  forceDecision?: PermissionDecision
) => Promise<PermissionDecision>;

权限检查是一个多阶段流水线。首先查询静态规则——这些规则来自用户配置文件、项目设置、企业策略等多个数据源,规定了哪些工具操作总是允许、总是拒绝或总是需要确认。如果判定结果是"需要确认",且当前处于 auto 模式,系统会调用一个 AI 分类器(Transcript 分类器,本身也是一次模型调用)结合对话上下文评估这次操作:分类器放行就直接执行,判定拦截就直接拒绝,并把原因反馈给模型。不在 auto 模式,或分类器不可用等情况下,才弹出交互式权限确认对话框,由用户做最终决策(完整链路见第 9 章 §9.5)。只有当 result.behavior === "allow" 时,工具才会被真正执行。

这种多层级的判定管线是在两个看似矛盾的目标之间求平衡:让大多数安全操作自动通过(用户体验流畅),让危险操作被拦下或交给用户确认(安全边界)。

第七站:工具执行

假设模型决定调用 FileWrite 工具来创建排序函数文件。工具执行发生在 src/services/tools/toolExecution.ts 中的 runToolUse() 函数。这是一个精心设计的多阶段流水线,每个阶段都有明确的职责和失败处理策略。执行过程经历五个阶段(与第 7 章 §7.3 一致):

  1. 输入校验:先用工具的 Zod schema 解析参数,再调用工具的 validateInput() 方法。这一步检查输入在技术上是否成立,如要编辑的文件是否存在、待替换的字符串能否在文件中找到(2.1.89 的 cli.js 里有 String to replace not found in file. 这类报错)。与权限检查不同,输入校验关注的是"能不能做"(技术可行性),而不是"允不允许做"(安全策略);唯一的交叉是 Read、Edit、Write 在这一步会先查一次 deny 规则,命中即提前拒绝(第 8 章 §8.6.1)。

  2. PreToolUse 钩子:执行用户在 settings.json 中配置的前置钩子脚本。例如,用户可能配置了一个钩子,在每次文件写入前检查是否符合团队的代码规范。钩子可以选择阻止工具执行,也可以通过 updatedInput 修改工具的输入参数。

  3. 权限检查:调用 canUseTool,走上一站描述的判定管线。钩子返回的 allow 不能绕过配置里的 deny / ask 规则。

  4. 工具调用:调用工具的 call() 方法,执行实际操作。对于 FileWrite,这意味着将内容写入指定路径的文件。执行过程中,工具可以通过 onProgress 回调报告进度,这些进度信息会实时显示在终端上。

  5. PostToolUse 钩子:执行后置钩子脚本。常见用途包括运行代码格式化工具(如 Prettier)、执行 lint 检查、更新索引等。钩子可以通过 additionalContext 给模型补充信息;但要替换工具的输出(updatedMCPToolOutput),在 2.1.89 里只对 MCP 工具生效,对 FileWrite 这样的内建工具不起作用。

工具的执行结果被包装成 ToolResult:

// src/Tool.ts
export type ToolResult<T> = {
  data: T;
  newMessages?: (UserMessage | AssistantMessage | AttachmentMessage)[];
  contextModifier?: (context: ToolUseContext) => ToolUseContext;
};

第八站:结果返回与循环继续

工具执行的结果被转换为 tool_result 类型的用户消息,追加到消息历史中。这是 Claude API 的协议要求——每个 tool_use 块必须有一个对应的 tool_result 块,它们通过共同的 tool_use_id 关联。这种配对机制确保模型能准确知道每次工具调用的结果。

然后 queryLoop 的 while(true) 进入下一次迭代:带着包含工具结果的完整消息历史,再次调用 API。模型收到工具执行结果后,可能有几种反应:

  • 任务完成:模型返回一条纯文本响应(如 "我已经为你创建了 sort.py 文件,包含快速排序的实现..."),此时 needsFollowUp 保持 false,经过 Stop 钩子等停止检查后循环终止,返回 { reason: 'completed' } 这样的 Terminal(API 侧的 stop_reason 是 end_turn,两者不是一回事)。

  • 需要更多操作:模型认为任务尚未完成,继续返回工具调用。例如,写完文件后模型可能还想运行测试来验证排序函数的正确性,这会触发 BashTool 来执行 python sort.py。

  • 需要用户确认:模型可能通过 AskUserQuestionTool 向用户提问,例如 "你希望排序函数支持自定义比较函数吗?"

整个过程可能经历多个轮次。在我们的例子中,一个典型的执行轨迹可能是:

轮次1: 模型调用 FileWrite 创建 sort.py       -> 工具返回 "文件创建成功"
轮次2: 模型调用 Bash 执行 python sort.py     -> 工具返回执行结果
轮次3: 模型返回文本 "排序函数已创建并验证通过" -> 循环结束

完整旅程图

用户输入: "帮我写一个快速排序函数"
    |
    v
[cli.tsx] CLI 入口 --> [main.tsx] 主编排器
    |
    v
[replLauncher.tsx] REPL 启动 --> React/Ink UI
    |
    v
[messages.ts] createUserMessage() --> 标准化消息
    |
    v
[REPL.tsx 直接调用 query();SDK / -p 经 QueryEngine.ts submitMessage()]
    |
    v
[queryContext.ts] fetchSystemPromptParts()
    |-- getSystemPrompt()    // 核心行为指令
    |-- getUserContext()      // CLAUDE.md 等
    |-- getSystemContext()    // Git 状态等
    |
    v
[query.ts] query() --> Generator 驱动的循环
    |
    v
[claude.ts] callModel() --> 流式 API 调用
    |
    v
 API 响应包含 tool_use?
    |           |
   否          是
    |           |
    v           v
 yield 文本  [toolOrchestration.ts] runTools()
    |           |
    |           v
    |     [toolExecution.ts] runToolUse()
    |       校验 -> PreToolUse -> 权限检查 -> call() -> PostToolUse
    |       (权限检查即 canUseTool,交互模式由 useCanUseTool.tsx 提供)
    |           |
    |           v
    |     工具结果 --> tool_result 消息
    |           |
    |           +---> 回到 queryLoop 下一轮迭代
    |
    v
 最终文本输出到终端

下面是这条消息旅程的流程图,展示了从用户输入到最终输出的完整数据流:

flowchart TD
    A["用户输入: 帮我写一个快速排序函数"] --> B["cli.tsx: CLI 入口"]
    B --> C["main.tsx: 主编排器"]
    C --> D["replLauncher.tsx: REPL 启动 React/Ink UI"]
    D --> E["messages.ts: createUserMessage 消息标准化"]
    E --> F["会话层:REPL.tsx 直接调用 query()<br/>SDK / -p 经 QueryEngine.submitMessage"]
    F --> G["queryContext.ts: fetchSystemPromptParts"]
    G --> G1["getSystemPrompt 核心行为指令"]
    G --> G2["getUserContext CLAUDE.md"]
    G --> G3["getSystemContext Git状态"]
    G1 & G2 & G3 --> H["query.ts: Generator 驱动的循环"]
    H --> I["claude.ts: callModel 流式API调用"]
    I --> J{"API响应包含 tool_use?"}
    J -- "否" --> K["yield 文本输出到终端"]
    J -- "是" --> L["toolOrchestration.ts: runTools"]
    L --> N["toolExecution.ts: runToolUse<br/>校验 / PreToolUse"]
    N --> M["canUseTool 权限检查<br/>(useCanUseTool.tsx)"]
    M --> P["tool.call() / PostToolUse"]
    P --> O["工具结果 -> tool_result 消息"]
    O --> H

2.2 六大核心子系统

了解了消息的完整旅程后,我们把视角拉远,看看整个系统由哪些子系统组成,以及它们之间的职责边界。本节只展开最核心的六个;LSP(第 13 章)、多 Agent 协调(第 14 章)、Skill 与插件(第 15 章)、上下文管理(第 16 章)、终端 UI(第 17 章)在 2.3 节的目录导航里给出位置,细节留给各自的章节。

下面的架构图展示了六大核心子系统之间的关系与数据流向:

graph TB
    subgraph CLI["CLI 入口与启动系统"]
        cli["cli.tsx"]
        main["main.tsx"]
        init["init.ts"]
    end
    subgraph QE["Query 引擎"]
        engine["QueryEngine.ts<br/>会话管理"]
        query["query.ts<br/>核心循环"]
    end
    subgraph TS["工具系统"]
        tool["Tool.ts 类型定义"]
        orch["toolOrchestration.ts<br/>编排层"]
        exec["toolExecution.ts<br/>执行层"]
        tools["40+ 工具实现"]
    end
    subgraph PS["权限系统"]
        perm["permissions.ts"]
        hook["useCanUseTool.tsx"]
        classifier["bashClassifier.ts"]
    end
    subgraph MCP["MCP 集成"]
        client["mcp/client.ts"]
        config["mcp/config.ts"]
        mcptool["MCPTool 封装"]
    end
    subgraph BR["Remote Control 桥接"]
        bmain["bridgeMain.ts"]
        session["sessionRunner.ts"]
        msg["bridgeMessaging.ts"]
    end
    CLI --> QE
    QE --> TS
    TS --> PS
    MCP --> TS
    BR --> QE
    orch --> exec
    exec --> hook
    mcptool --> tool

2.2.1 CLI 入口与启动系统

核心文件:src/entrypoints/cli.tsx、src/main.tsx、src/entrypoints/init.ts

启动系统负责将 Claude Code 从一个静态的代码包变成一个运行中的交互式 Agent。它的职责包括:

  • 快速路径分流:cli.tsx 通过检查 process.argv 为 --version 等快速操作提供零依赖或最小依赖的路径,无需加载完整运行时。(第 3 章讲到的 --dump-system-prompt 同属此类,但它受 feature('DUMP_SYSTEM_PROMPT') 守卫、只存在于内部构建,2.1.89 的 cli.js 里查无此名。)

  • 并行预热:main.tsx 在模块导入阶段就启动 MDM 配置读取和 Keychain 凭证预取,利用模块 evaluate 时间做有用的 I/O 工作。

  • 初始化编排:init.ts 中的 init() 函数(被 memoize 包装以防重复调用)执行环境检测、配置加载、代理配置、优雅退出处理、遥测初始化等一系列启动任务。

  • 迁移系统:main.tsx 包含版本化的迁移逻辑(CURRENT_MIGRATION_VERSION = 11),确保用户的本地配置随版本升级平滑迁移。

  • 模式分流:根据启动参数决定进入交互模式(REPL)还是非交互模式(-p 参数的 headless 执行),或是 MCP 服务器模式、SDK 模式等。

启动系统的设计理念是"越早开始有用的工作越好"。这从第一行代码就能看出来——profileCheckpoint('main_tsx_entry') 不仅是性能监控,更是对启动延迟的严肃承诺。整个启动序列被精心安排成一个瀑布流:网络 I/O 操作(如 API 预连接、凭证获取)在最早的时间点启动,CPU 密集操作(如配置解析、工具初始化)填补 I/O 等待的空隙。

一个有趣的细节是调试保护机制。main.tsx 在外部构建中会检测是否有调试器附加(process.execArgv 或 NODE_OPTIONS 里的 --inspect / --debug,以及 inspector.url()),检测到就以退出码 1 直接退出——2.1.89 的 cli.js 里 main_tsx_imports_loaded 打点之后紧跟着的正是这一判断。

2.2.2 Query 引擎

核心文件:src/QueryEngine.ts、src/query.ts、src/query/config.ts、src/query/deps.ts

Query 引擎是整个系统的心脏。它管理着一个对话的完整生命周期,核心抽象是 QueryEngine 类:

// src/QueryEngine.ts
export class QueryEngine {
  private config: QueryEngineConfig;
  private mutableMessages: Message[];
  private abortController: AbortController;
  private totalUsage: NonNullableUsage;
  private readFileState: FileStateCache;

  async *submitMessage(
    prompt: string | ContentBlockParam[],
    options?: { uuid?: string; isMeta?: boolean },
  ): AsyncGenerator<SDKMessage, void, unknown> {
    // ...
  }
}

QueryEngine 的设计有几个关键特征:

一,它是有状态的。 一个 QueryEngine 实例对应一个对话。mutableMessages 数组在多次 submitMessage() 调用之间持续积累,实现多轮对话。readFileState 缓存已读文件的内容,避免重复读取。

二,它与渲染解耦。 QueryEngine 通过 AsyncGenerator 向外 yield 事件,不直接操作 UI。交互式 REPL 并不经过 QueryEngine,而是自己直接消费 query();QueryEngine 服务的是 SDK / headless 模式——两者共用的是下层同一个 query() 循环。

三,它封装了上下文管理。 自动压缩(auto-compact)、微压缩(microcompact)、裁剪压缩(snip-compact)等上下文窗口管理策略都集成在 query 循环内部,对上层调用者透明。

QueryEngine 和 query() 函数的关系是一种清晰的分层:QueryEngine 负责会话级状态管理和外部接口,而 query() 函数实现了单轮对话的核心循环逻辑(API 调用 -> 工具执行 -> 继续/终止)。QueryEngine.submitMessage() 内部调用 query() 并负责消息的前后处理——包括用户输入的预处理(如斜杠命令解析、附件注入)和轮次完成后的后处理(如对话记录持久化、使用量统计更新)。

这种分层的价值在 SDK 模式下尤为明显:SDK 调用者只需创建一个 QueryEngine 实例并反复调用 submitMessage(),而不需要关心系统提示词组装、上下文窗口管理、权限状态追踪等复杂的内部机制。QueryEngine 成为了一个完整的、有状态的对话 API。

2.2.3 工具系统

核心文件:src/Tool.ts、src/tools.ts、src/tools/ 目录、src/services/tools/

工具系统是 Claude Code 区别于普通聊天机器人的核心能力。Tool 类型定义了一个工具必须实现的完整契约:

// src/Tool.ts (简化)
export type Tool<Input, Output, P> = {
  readonly name: string;
  readonly inputSchema: Input;
  call(args, context, canUseTool, parentMessage, onProgress?):
    Promise<ToolResult<Output>>;
  description(input, options): Promise<string>;
  isConcurrencySafe(input): boolean;
  isEnabled(): boolean;
  isReadOnly(input): boolean;
  checkPermissions(input, context): Promise<PermissionResult>;
  validateInput?(input, context): Promise<ValidationResult>;
  maxResultSizeChars: number;
  // ...
};

这个接口的设计值得仔细品味:

  • isConcurrencySafe(input):注意参数是 input 而不是无参——同一个工具在不同输入下可能有不同的并发安全性。例如 BashTool 执行 ls 是安全的,但执行 rm 就不是。

  • isReadOnly(input):同理,只读性取决于具体输入。这个方法被工具编排器用来决定并发策略。

  • maxResultSizeChars:工具结果超过此阈值时,会被持久化到磁盘文件,Claude 只收到一个摘要和文件路径。设为 Infinity 的工具(如 FileRead)表示结果永不持久化——因为持久化一个文件读取结果会导致"读文件->保存到文件->读文件"的死循环。

  • checkPermissions 与 validateInput:两者职责不同。validateInput 判断输入格式是否合法(如路径是否存在),checkPermissions 判断操作是否被允许(如是否在允许的目录内)。

工具的注册在 src/tools.ts 的 getAllBaseTools() 函数中:

// src/tools.ts(简化,省略了部分无条件工具,较完整的列表见第 6 章 §6.4)
export function getAllBaseTools(): Tools {
  return [
    AgentTool, TaskOutputTool, BashTool,
    ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
    FileReadTool, FileEditTool, FileWriteTool,
    NotebookEditTool, WebFetchTool, TodoWriteTool,
    WebSearchTool, SkillTool, EnterPlanModeTool,
    // 条件工具...
    ...(isWorktreeModeEnabled()
      ? [EnterWorktreeTool, ExitWorktreeTool] : []),
    ...(isToolSearchEnabledOptimistic()
      ? [ToolSearchTool] : []),
    // ...
  ];
}

工具列表是动态组合的。哪些工具可用取决于运行时环境(是否有嵌入式搜索工具)、用户配置(是否启用 Worktree 模式)、特性标志(是否启用 ToolSearch)等条件。最终,getTools() 函数还会通过 filterToolsByDenyRules() 根据权限规则进一步过滤。

工具执行的编排层位于 src/services/tools/ 目录下,包含四个关键文件:

文件 职责
toolOrchestration.ts 批量工具调用的编排,分区并发/串行
StreamingToolExecutor.ts 流式执行器,在 API 响应流式到达时就开始执行工具
toolExecution.ts 单个工具的完整执行流程(校验->PreToolUse->权限->执行->PostToolUse)
toolHooks.ts PreToolUse / PostToolUse 钩子的运行

StreamingToolExecutor 是一个特别巧妙的优化。传统的做法是等 API 完整响应后再开始执行工具,但 StreamingToolExecutor 在 API 响应还在流式传输时就开始执行已收到的工具调用:

// src/services/tools/StreamingToolExecutor.ts
export class StreamingToolExecutor {
  addTool(block: ToolUseBlock, assistantMessage: AssistantMessage): void {
    // 工具到达时立即加入队列并尝试执行
  }
  getCompletedResults(): MessageUpdate[] {
    // 返回已完成的工具结果,保持顺序
  }
}

2.2.4 权限系统

核心文件:src/hooks/useCanUseTool.tsx、src/utils/permissions/permissions.ts、src/utils/permissions/ 目录

权限系统保护用户免受 AI 误操作的风险。它的核心是 src/utils/permissions/permissions.ts 中的 hasPermissionsToUseTool() 函数,以及 useCanUseTool Hook。

权限系统的架构是一个多层判定管线,以下流程图展示了完整的权限决策链路:

flowchart TD
    A["工具调用请求"] --> B{"工具级 deny / ask 规则"}
    B -- "deny 命中" --> DENY["拒绝执行"]
    B -- "ask 命中" --> D
    B -- "未命中" --> C{"工具自身 checkPermissions"}
    C -- "明确允许" --> ALLOW["允许执行"]
    C -- "明确拒绝" --> DENY
    C -- "要求确认" --> D
    C -- "未表态" --> P{"bypassPermissions 模式<br/>或 allow 规则命中?"}
    P -- "是" --> ALLOW
    P -- "否" --> D{"auto 模式?"}
    D -- "否" --> UI["交互式权限确认 UI"]
    D -- "是" --> E{"AI 分类器评估"}
    E -- "放行" --> ALLOW
    E -- "拦截" --> DENY
    E -- "不可用 / 对话过长" --> UI
    UI -- "用户批准" --> ALLOW
    UI -- "用户拒绝" --> DENY
    UI -- "始终允许" --> F["生成持久 allow 规则"] --> ALLOW

这是简化图:dontAsk 模式会把"需要确认"直接改判为拒绝;敏感路径等安全检查即使在 auto 模式下也可能不交给分类器、而是要求人工确认;分类器不可用时也可能按开关直接拒绝(fail closed)。完整链路见第 9 章 §9.5。

权限上下文 ToolPermissionContext 定义了权限系统的完整状态:

// src/Tool.ts
export type ToolPermissionContext = DeepImmutable<{
  mode: PermissionMode;
  additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>;
  alwaysAllowRules: ToolPermissionRulesBySource;
  alwaysDenyRules: ToolPermissionRulesBySource;
  alwaysAskRules: ToolPermissionRulesBySource;
  isBypassPermissionsModeAvailable: boolean;
  isAutoModeAvailable?: boolean;
  shouldAvoidPermissionPrompts?: boolean;
  // ...
}>;

注意 DeepImmutable 类型包装——权限上下文是深度不可变的。修改权限状态不是原地修改,而是通过 setAppState 产生新的状态对象。这防止了权限状态被意外篡改。

src/utils/permissions/ 目录下的文件形成了一个完整的权限子系统:

文件 职责
PermissionMode.ts 权限模式的显示信息与切换逻辑(共七种:default / acceptEdits / plan / bypassPermissions / dontAsk / auto / bubble,类型定义在 src/types/permissions.ts,详见第 9 章;2.1.89 的 claude --help 里 --permission-mode 可选其中除 bubble 外的六种,auto 在公开包里可用)
PermissionRule.ts 权限规则的类型定义
PermissionResult.ts 权限判定结果的类型
permissions.ts 核心权限判定逻辑
permissionsLoader.ts 从配置文件加载权限规则
bashClassifier.ts Bash 命令与 prompt: 语义规则的匹配分类器(受 BASH_CLASSIFIER 开关控制,公开构建中为空实现,第 9 章 §9.4.1)
yoloClassifier.ts auto 模式的 Transcript 分类器(第 9 章 §9.4.2)
denialTracking.ts 分类器拒绝次数追踪(超过阈值时回退到人工审批)
pathValidation.ts 文件路径安全校验

2.2.5 MCP 集成

核心文件:src/services/mcp/client.ts、src/services/mcp/config.ts、src/services/mcp/types.ts

MCP(Model Context Protocol)是 Anthropic 提出的标准协议,允许 Claude Code 连接外部工具服务器。MCP 集成子系统的职责是将外部 MCP 服务器提供的工具无缝融入 Claude Code 的工具系统。

src/services/mcp/client.ts 是 MCP 客户端的核心实现,它使用 @modelcontextprotocol/sdk 与 MCP 服务器通信。最常见的是下面三种传输方式(第 11 章还讲了 WebSocket、进程内与 SDK 控制通道等传输):

// src/services/mcp/client.ts (import 部分即可看出)
import { StdioClientTransport } from
  '@modelcontextprotocol/sdk/client/stdio.js';
import { SSEClientTransport } from
  '@modelcontextprotocol/sdk/client/sse.js';
import { StreamableHTTPClientTransport } from
  '@modelcontextprotocol/sdk/client/streamableHttp.js';
  • Stdio 传输:MCP 服务器作为子进程启动,通过标准输入/输出通信
  • SSE 传输:通过 HTTP Server-Sent Events 连接远程服务器(MCP 规范 2025-03-26 版起以 Streamable HTTP 取代它,保留用于兼容旧服务器)
  • StreamableHTTP 传输:更新的 HTTP 传输方案,支持双向流式通信

MCP 工具在 Claude Code 中被封装为标准的 Tool 对象(通过 MCPTool 类),与内建工具使用完全相同的权限检查和执行流程。工具名称遵循 mcp__<serverName>__<toolName> 的命名约定,通过 mcpInfo 字段保留原始的服务器名和工具名。这种封装的优雅之处在于:模型无需知道一个工具是内建的还是来自 MCP 服务器——它们在工具列表中的表现完全一致,使用相同的调用协议。

src/services/mcp/config.ts 负责从多个作用域(enterprise / local / project / user / plugin)解析和合并 MCP 服务器配置,合并优先级为 plugin < user < project < local;enterprise 作用域一旦存在即独占,其余作用域全部失效——企业因此可以通过策略配置精确限定哪些 MCP 服务器被允许,防止用户连接到未经审查的外部服务。合并细节见第 11 章。

除了工具之外,MCP 还支持资源(Resources)和提示(Prompts)。资源是 MCP 服务器暴露的只读数据源,如数据库查询结果或 API 端点的响应。Claude Code 通过 ListMcpResourcesTool 和 ReadMcpResourceTool 让模型可以发现和读取这些资源。提示是服务器预定义的交互模板,Claude Code 把它们转换成 /mcp__<server>__<prompt> 形式的斜杠命令,从而扩展了自己的命令系统(第 11 章 §11.6.1)。

2.2.6 Remote Control 桥接

核心文件:src/bridge/ 目录

src/bridge/ 实际对应的产品功能是 Remote Control(第 12 章「Remote Control 桥接架构」),不是 IDE 集成。2.1.89 的 cli.js 里,隐藏子命令 claude remote-control(别名 rc)的描述是 "Connect your local environment for remote-control sessions via claude.ai/code",会话内还有同名斜杠命令 /remote-control;按官方文档(核对于 2026-09-24),它让你从 claude.ai/code 或 Claude 手机 App 接管本机上运行的 Claude Code 会话,代码执行和文件访问始终留在本机。VS Code / JetBrains 扩展与本机 CLI 的直连(--ide 参数、sse-ide / ws-ide 两种 MCP 传输)是另一条路径,不经过这里。它不是一个简单的 API 调用层,而是一个完整的会话管理和消息中继系统。

src/bridge/bridgeMain.ts 是 Bridge 的入口,其导入就展示了它的复杂性:

// src/bridge/bridgeMain.ts
import { createBridgeApiClient } from './bridgeApi.js';
import { createCapacityWake } from './capacityWake.js';
import { createTokenRefreshScheduler } from './jwtUtils.js';
import { createSessionSpawner } from './sessionRunner.js';
import { getTrustedDeviceToken } from './trustedDevice.js';

Bridge 目录下的文件各司其职:

文件 职责
bridgeApi.ts 与 Bridge 服务器的 HTTP API 通信
bridgeConfig.ts Bridge 连接配置
bridgeMessaging.ts 消息的序列化与传输
bridgePermissionCallbacks.ts 权限请求转发到远端界面处理
sessionRunner.ts Claude Code 会话子进程的生命周期管理
capacityWake.ts 容量唤醒信号(会话结束后立即唤醒轮询,接新任务)
jwtUtils.ts JWT Token 的刷新调度
trustedDevice.ts 设备信任认证
inboundMessages.ts 来自远端的入站消息处理
inboundAttachments.ts 来自远端的附件下载与转换

Bridge 有两种运行形态(第 12 章 §12.1.2):claude remote-control 启动的独立 Bridge 进程向服务器注册为一个"环境",每接到一个远程会话就 spawn 一个 Claude Code 子进程去跑,属于"会话即进程";在已有的交互会话里执行 /remote-control,则是由当前 REPL 进程自己建立 Bridge 连接。"会话即进程"的好处是进程隔离——一个会话的崩溃不会影响其他会话,每个会话的文件状态缓存、权限上下文、对话历史也都是独立的。

Bridge 的消息流是双向的:从远端(claude.ai/code 网页或手机 App)到本机传递用户输入与控制指令;从本机到远端传递流式输出、工具活动与权限确认请求。bridgePermissionCallbacks.ts 特别值得关注——它让权限确认可以跨进程、跨网络转发到远端界面,用户在浏览器或手机上就能审批或拒绝工具操作,而不必回到本机终端。

2.3 源码目录结构导航

理解一个大型项目的第一步是建立空间感。以下是 src/ 目录下每个关键目录的用途,作为你后续深入阅读的导航地图:

src/
|-- main.tsx                 # 主编排器,CLI 参数解析和启动逻辑
|-- query.ts                 # 核心 query 循环 (Generator)
|-- QueryEngine.ts           # 会话级 query 引擎
|-- Tool.ts                  # Tool 类型定义和工具相关类型
|-- tools.ts                 # 工具注册表 (getAllBaseTools)
|-- commands.ts              # 斜杠命令注册
|-- context.ts               # 系统上下文和用户上下文收集
|-- cost-tracker.ts          # API 调用成本追踪
|
|-- entrypoints/             # 入口点
|   |-- cli.tsx              # CLI 主入口 (快速路径分流)
|   |-- init.ts              # 初始化逻辑 (环境检测, 配置加载)
|   |-- sdk/                 # SDK 入口 (程序化调用)
|   |-- mcp.ts               # MCP 服务器模式入口
|
|-- cli/                     # CLI 入口与命令行行为
|-- commands/                # CLI 命令实现(80+ 斜杠命令,与顶层 commands.ts 注册表配合)
|
|-- tools/                   # 工具实现 (40 多个内置工具)
|   |-- BashTool/            # Shell 命令执行
|   |-- FileReadTool/        # 文件读取
|   |-- FileEditTool/        # 文件编辑 (基于差异)
|   |-- FileWriteTool/       # 文件写入
|   |-- GrepTool/            # 内容搜索 (ripgrep)
|   |-- GlobTool/            # 文件路径匹配
|   |-- AgentTool/           # 子 Agent 启动
|   |-- MCPTool/             # MCP 工具封装
|   |-- WebFetchTool/        # URL 内容获取
|   |-- WebSearchTool/       # 网页搜索
|   |-- ToolSearchTool/      # 延迟加载的工具搜索
|   |-- SkillTool/           # 技能执行
|   |-- NotebookEditTool/    # Jupyter Notebook 编辑
|   |-- LSPTool/             # Language Server Protocol 集成
|   |-- shared/              # 工具间共享逻辑
|   +-- ...
|
|-- services/                # 服务层
|   |-- api/                 # Claude API 调用层
|   |-- mcp/                 # MCP 客户端和配置
|   |-- tools/               # 工具编排和执行 (orchestration)
|   |-- compact/             # 上下文压缩 (auto/micro/snip/reactive)
|   |-- analytics/           # 遥测和分析
|   |-- lsp/                 # LSP 服务器管理
|   |-- oauth/               # OAuth 认证
|   |-- plugins/             # 插件管理
|   |-- policyLimits/        # 企业策略限制
|   |-- tips/                # 提示建议
|   +-- ...
|
|-- bridge/                  # Remote Control 桥接
|   |-- bridgeMain.ts        # Bridge 入口
|   |-- bridgeApi.ts         # Bridge API 客户端
|   |-- sessionRunner.ts     # 会话进程管理
|   +-- ...
|
|-- hooks/                   # React Hooks (权限, 状态)
|   |-- useCanUseTool.tsx    # 工具权限判定 Hook
|   +-- toolPermission/      # 权限判定子模块
|
|-- utils/                   # 工具函数 (最大的目录)
|   |-- permissions/         # 权限系统核心逻辑
|   |-- messages.ts          # 消息创建和标准化
|   |-- api.ts               # API 辅助函数
|   |-- queryContext.ts       # 系统提示词组装
|   |-- model/               # 模型选择和配置
|   |-- settings/            # 设置系统
|   |-- hooks.ts             # 用户自定义钩子执行
|   |-- Shell.ts             # Shell 命令执行
|   |-- tokens.ts            # Token 计数和估算
|   |-- git.ts               # Git 操作
|   +-- ...
|
|-- ink/                     # 自定义 Ink 渲染器(48 个文件,React reconciler + 双缓冲帧渲染,见第 17 章)
|-- components/              # React/Ink UI 组件
|-- screens/                 # 顶层屏幕 (REPL 等)
|-- state/                   # 应用状态管理
|   |-- AppStateStore.ts     # AppState 类型定义
|   |-- store.ts             # 状态存储
|   +-- ...
|
|-- constants/               # 常量定义
|   |-- prompts.ts           # 系统提示词模板
|   |-- tools.ts             # 工具相关常量
|   +-- ...
|
|-- types/                   # 类型定义
|   |-- message.ts           # 消息类型
|   |-- permissions.ts       # 权限类型
|   +-- ...
|
|-- coordinator/             # 多 Agent 协调器(feature('COORDINATOR_MODE') 内部开关,公开包已剥离,见第 14 章)
|-- skills/                  # 内建技能
|-- plugins/                 # 插件系统
|-- migrations/              # 配置迁移脚本
|-- bootstrap/               # 启动状态管理
|-- query/                   # query 子模块
|   |-- config.ts            # query 配置快照
|   |-- deps.ts              # query 依赖注入
|   |-- stopHooks.ts         # 停止钩子处理
|   +-- tokenBudget.ts       # Token 预算管理
+-- ...

2.4 核心架构模式

在六大子系统和目录结构的背后,有几个贯穿整个代码库的架构模式。理解它们,你就能预测新代码应该放在哪里,以及现有代码为什么要那样组织。

2.4.1 Generator 驱动的流式管道

Claude Code 最核心的架构模式是使用 JavaScript 的 async function*(异步 Generator)构建数据管道。从最内层的 API 流式响应,到最外层的终端渲染,整个链路都是 Generator 驱动的。

以下时序图以 SDK / -p 路径为例,展示 Generator 管道中各层之间的 yield 传递过程(交互式 REPL 没有 QueryEngine 这一层,由 REPL.tsx 直接 for await 消费 query()):

sequenceDiagram
    participant UI as SDK / -p 消费者
    participant QE as QueryEngine.submitMessage
    participant Q as query / queryLoop
    participant API as claude.ts callModel
    participant Tools as toolOrchestration runTools
    UI->>QE: 用户输入
    QE->>Q: yield* query(params)
    Q->>API: for await callModel(messages)
    API-->>Q: yield StreamEvent (文本/思考)
    Q-->>QE: yield StreamEvent
    QE-->>UI: yield SDKMessage (实时输出)
    API-->>Q: yield tool_use 块
    Q->>Tools: yield* runTools(toolUseBlocks)
    Tools-->>Q: yield MessageUpdate (工具结果)
    Q-->>QE: yield tool_result 消息
    QE-->>UI: yield SDKMessage
    Note over Q: while(true) 循环继续
    Q->>API: 再次 callModel (含工具结果)
    API-->>Q: yield 最终文本
    Q-->>QE: yield 文本 + return Terminal
    QE-->>UI: 对话轮次结束

以下是原始的层次结构示意:

claude.ts (API Stream)
  --> query.ts (queryLoop)
    --> QueryEngine.ts (submitMessage) --> SDK / -p 输出
    --> REPL.tsx (for await 消费)     --> 终端 UI 渲染

除最外层的消费者外,每一层都是一个 async function*,通过 yield 向上层传递事件,通过 yield* 将内层 Generator 的输出透传。这种模式带来了三个关键好处:

一,天然流式。 用户能实时看到 Claude 的输出,而不是等到完整响应才显示。文本、思考过程、工具调用进度都能实时渲染。

二,惰性求值。 Generator 是拉模型(pull-based)——只有消费者请求下一个值时,生产者才会执行到下一个 yield。这意味着如果用户中断(Esc),已经排队但尚未消费的消息不会被处理,避免了不必要的计算。

三,组合性。 Generator 可以通过 yield* 进行组合。query() 函数将内部的 queryLoop() Generator 透传出去,同时在完成时做清理工作。这比回调嵌套或 Promise 链更清晰。

// 组合示例:query 函数透传 queryLoop 的所有输出
export async function* query(params: QueryParams) {
  const consumedCommandUuids: string[] = [];
  const terminal = yield* queryLoop(params, consumedCommandUuids);
  // 清理工作只在循环正常结束时执行
  for (const uuid of consumedCommandUuids) {
    notifyCommandLifecycle(uuid, 'completed');
  }
  return terminal;
}

2.4.2 工具即自描述对象

以下类图展示了 Tool 接口的核心结构以及典型工具的实现关系:

classDiagram
    class Tool~Input,Output,P~ {
        +readonly name: string
        +readonly inputSchema: Input
        +aliases: string[]
        +searchHint: string
        +maxResultSizeChars: number
        +call(args, context, canUseTool, parentMessage, onProgress) ToolResult~Output~
        +description(input, options) string
        +isConcurrencySafe(input) boolean
        +isReadOnly(input) boolean
        +isEnabled() boolean
        +checkPermissions(input, context) PermissionResult
        +validateInput(input, context) ValidationResult
    }
    class BashTool {
        +name = "Bash"
        +call() 执行Shell命令
        +isConcurrencySafe() 根据命令判断
    }
    class FileReadTool {
        +name = "Read"
        +call() 读取文件
        +isConcurrencySafe() true
        +isReadOnly() true
    }
    class FileWriteTool {
        +name = "Write"
        +call() 写入文件
        +isConcurrencySafe() false
        +isReadOnly() false
    }
    class MCPTool {
        +name = "mcp__server__tool"
        +call() 转发MCP调用
        +mcpInfo: MCPInfo
    }
    Tool <|.. BashTool
    Tool <|.. FileReadTool
    Tool <|.. FileWriteTool
    Tool <|.. MCPTool

传统的工具系统通常是"注册函数+配置文件"的模式,但 Claude Code 的工具是自描述对象——每个工具包含了运行它所需的全部元信息。以 BashTool 为例:

// src/tools/BashTool/BashTool.tsx (结构示意)
export const BashTool: Tool = buildTool({
  name: 'Bash',
  inputSchema: z.object({
    command: z.string(),
    timeout: z.number().optional(),
  }),
  async call(args, context, canUseTool, parentMessage, onProgress) {
    // 执行 shell 命令...
  },
  async description(input, options) {
    return `执行命令: ${input.command}`;
  },
  isConcurrencySafe(input) {
    // 根据命令内容判断是否可以并发
    return isReadOnlyCommand(input.command);
  },
  isReadOnly(input) { /* ... */ },
  isEnabled() { return true; },
  async checkPermissions(input, context) { /* ... */ },
  async validateInput(input, context) { /* ... */ },
  maxResultSizeChars: 100_000,
});

这种设计的优势是工具的添加和删除不需要修改编排层代码。要添加一个新工具,只需要创建一个实现 Tool 接口的对象,然后在 tools.ts 的数组中注册即可。编排层通过接口契约(isConcurrencySafe、isReadOnly、checkPermissions 等方法)自动适配新工具的行为。

更进一步,ToolSearchTool 和 shouldDefer 属性实现了工具的延迟加载。在 ToolSearch 模式下,被标记为延迟的工具仍随请求发给 API,但带上 defer_loading: true,模型起初只知道有这些工具、看不到完整定义;模型调用 ToolSearch 后,工具结果里返回 tool_reference 块,由 API 把对应工具的完整定义展开进上下文(2.1.89 的 cli.js 里这两个字段名都在)。这样可以减少上下文窗口的占用。

2.4.3 权限上下文栈

权限系统不是一个简单的 "allow/deny" 判定器,而是一个具有上下文感知能力的分层系统。权限规则来自多个源(第 9 章 §9.3.1 列了八种),常见的有:

企业策略       (policySettings)   managed-settings 等下发,用户不可改
命令行参数     (cliArg / flagSettings)  --allowedTools、--settings 等
本地项目设置   (localSettings)    .claude/settings.local.json
共享项目设置   (projectSettings)  .claude/settings.json
用户设置       (userSettings)     ~/.claude/settings.json
会话临时授权   (session)          对话框里选"本次会话允许"

每个源可以定义 alwaysAllow、alwaysDeny、alwaysAsk 三类规则。要注意,这些规则不是"高优先级来源覆盖低优先级来源":各来源的规则汇总到一起,按 deny → ask → allow 的顺序判定(第 9 章 §9.3.3),所以任何一个来源里的 deny 都不会被别处的 allow 推翻。官方文档(核对于 2026-09-24)的表述相同:用户级 deny 能挡住项目级 allow,反之亦然;托管策略里的规则任何层级都无法覆盖。规则匹配采用工具名+参数模式的方式,例如 Bash(git *) 表示允许所有以 git 开头的 Bash 命令。

权限上下文的不可变性(DeepImmutable)保证了规则的修改是显式的、可追踪的。整个状态通过 setAppState 函数式更新,产生新的快照,而不是原地修改。

ToolUseContext 中的 toolDecisions Map 记录了每次权限判定的结果,用于审计和调试:

toolDecisions?: Map<string, {
  source: string;
  decision: 'accept' | 'reject';
  timestamp: number;
}>;

2.4.4 特性标志与死代码消除

Claude Code 的源码中大量使用了 feature() 宏:

import { feature } from 'bun:bundle';

const reactiveCompact = feature('REACTIVE_COMPACT')
  ? require('./services/compact/reactiveCompact.js')
  : null;

const coordinatorModeModule = feature('COORDINATOR_MODE')
  ? require('./coordinator/coordinatorMode.js')
  : null;

feature() 不是运行时检查。它是 Bun 的编译期宏——在构建时,Bun 的打包器会根据构建配置将 feature('XYZ') 替换为 true 或 false。替换后,未命中的分支成为死代码,被打包器完全消除。注意 Bun 在这里扮演的是构建工具:npm 分发的 2.1.89 包里,cli.js 首行是 #!/usr/bin/env node,package.json 声明 engines.node >= 18.0.0,也就是说这个打包产物由 Node 运行。

这个模式在 src/tools.ts 中尤为密集:

const SleepTool = feature('PROACTIVE') || feature('KAIROS')
  ? require('./tools/SleepTool/SleepTool.js').SleepTool
  : null;

const cronTools = feature('AGENT_TRIGGERS')
  ? [require('./tools/ScheduleCronTool/CronCreateTool.js').CronCreateTool,
     /* ... */]
  : [];

这带来了两个好处:

一,产物精简。 外部构建不包含那些在外部构建里取值为 false 的功能代码,减小了包体积。反过来,开关在外部构建中为 true 的功能会被保留——例如 2.1.89 的 cli.js 里就能 grep 到 CronCreate,而 CLAUDE_CODE_COORDINATOR_MODE 查无此名。

二,边界清晰。 一个特性的所有代码都被 feature() 守卫包裹,你可以通过搜索特性名快速找到它影响的所有代码。

值得注意的是,process.env.USER_TYPE === 'ant' 也起到了类似的作用,将 Anthropic 内部专用功能(如 TungstenTool,在 2.1.89 的 cli.js 里查无此名)与外部版本隔离。

2.4.5 依赖注入与可测试性

src/query/deps.ts 展示了 Claude Code 的依赖注入策略。query() 函数的外部依赖(如 API 调用、UUID 生成、自动压缩)不是硬编码的,而是通过 QueryDeps 接口注入:

// src/query/deps.ts (结构示意;原文用 typeof 引用真实函数类型,见第 4 章 §4.2.1)
export type QueryDeps = {
  callModel: (...) => AsyncGenerator<...>;
  microcompact: (...) => Promise<...>;
  autocompact: (...) => Promise<...>;
  uuid: () => string;
};

export function productionDeps(): QueryDeps {
  return {
    callModel: /* 真实的 API 调用 */,
    microcompact: ,
    autocompact: /* 真实的压缩逻辑 */,
    uuid: randomUUID,
  };
}

在测试中,可以注入 mock 依赖来隔离测试 query 循环的逻辑,而无需真正调用 API。这种模式比全局 mock(如 jest.mock)更精确、更可维护。

2.5 架构全景图

将上述所有内容综合起来,以下是 Claude Code 的架构全景图:

+------------------------------------------------------------------+
|                   用户 (终端 / 远端 Web、App)                    |
+------------------------------------------------------------------+
       |                        ^                       |
       | 用户输入               | 流式输出              | 远程操作
       v                        |                       v
+------------------+   +------------------+   +------------------+
|  CLI 入口        |   |  React/Ink UI    |   |  RC Bridge       |
|  cli.tsx         |   |  REPL 组件        |   |  bridgeMain.ts   |
|  main.tsx        |   |  components/      |   |  sessionRunner   |
+------------------+   +------------------+   +------------------+
       |                        ^                       |
       | 初始化/参数解析        | yield SDKMessage       |
       v                        |                       v
+------------------------------------------------------------------+
|                    QueryEngine (会话管理)                          |
|  QueryEngine.ts: submitMessage() -> AsyncGenerator<SDKMessage>    |
+------------------------------------------------------------------+
       |
       | 消息 + 系统提示词 + 工具上下文
       v
+------------------------------------------------------------------+
|                    Query 循环 (核心引擎)                           |
|  query.ts: queryLoop() -> while(true) { ... yield ... }          |
|                                                                   |
|  +-------------------+  +------------------+  +----------------+ |
|  | 系统提示词组装      |  | 上下文管理        |  | Token 预算     | |
|  | queryContext.ts    |  | autocompact      |  | tokenBudget   | |
|  | prompts.ts        |  | microcompact     |  |               | |
|  | context.ts        |  | snipCompact      |  |               | |
|  +-------------------+  +------------------+  +----------------+ |
+------------------------------------------------------------------+
       |                        ^
       | API 请求               | 流式响应
       v                        |
+------------------------------------------------------------------+
|                    API 调用层                                      |
|  services/api/claude.ts: callModel()                              |
|  模型选择 | 重试 | 降级 | 缓存                                    |
+------------------------------------------------------------------+
       |
       | tool_use 块
       v
+------------------------------------------------------------------+
|                    工具编排层                                      |
|  services/tools/toolOrchestration.ts: runTools()                  |
|  services/tools/StreamingToolExecutor.ts                          |
|                                                                   |
|  并发分区: [只读工具并行] [写入工具串行]                            |
+------------------------------------------------------------------+
       |                        |
       v                        v
+-------------------+  +------------------+
|  权限系统          |  |  工具执行         |
|  useCanUseTool    |  |  toolExecution   |
|  permissions.ts   |  |  toolHooks.ts    |
|                   |  |                  |
|  静态规则匹配     |  |  validateInput   |
|  分类器判定       |  |  PreToolUse 钩子  |
|  交互式确认       |  |  tool.call()     |
|                   |  |  PostToolUse 钩子 |
+-------------------+  +------------------+
       |                        |
       v                        v
+------------------------------------------------------------------+
|                    工具实现层                                      |
|  tools/BashTool   | tools/FileEditTool | tools/AgentTool | ...   |
+------------------------------------------------------------------+
       |                                           |
       v                                           v
+-------------------+                    +------------------+
|  MCP 集成         |                    |  系统资源         |
|  services/mcp/    |                    |  文件系统/Shell   |
|  MCPTool 封装     |                    |  Git/网络         |
|  多传输协议       |                    |                  |
+-------------------+                    +------------------+

以下状态机图展示了 query 循环在处理一次完整对话时的状态转换:

stateDiagram-v2
    [*] --> BuildConfig: 进入 queryLoop
    BuildConfig --> PreprocessMessages: 构建 QueryConfig 快照
    PreprocessMessages --> CallAPI: snip / microcompact / collapse / autocompact
    CallAPI --> ProcessResponse: 流式调用 claude.ts
    ProcessResponse --> YieldText: 纯文本响应
    ProcessResponse --> ExtractToolUse: 包含 tool_use 块
    YieldText --> CheckBudget
    ExtractToolUse --> RunTools: 进入工具编排
    RunTools --> CollectResults: 工具执行完成
    CollectResults --> PreprocessMessages: needsFollowUp = true
    CheckBudget --> NudgeContinue: 预算未达 90%
    CheckBudget --> Terminal: 预算耗尽或 end_turn
    NudgeContinue --> PreprocessMessages: 注入续行消息
    Terminal --> [*]
    ProcessResponse --> ErrorRecovery: API 错误
    ErrorRecovery --> PreprocessMessages: 重试/降级/压缩
    ErrorRecovery --> Terminal: 不可恢复

数据流向说明:

  1. 用户输入从顶部进入,经过 CLI 入口到达会话层(SDK / -p 是 QueryEngine,交互式 REPL 由 REPL.tsx 直接调用 query)
  2. 会话层调用 query 循环:系统提示词在进入循环前组装好,循环内每轮先做上下文管理
  3. 带着完整消息历史调用 API,流式接收响应
  4. 如果响应包含 tool_use,进入工具编排层
  5. 工具编排层根据并发安全性分组,通过权限系统检查后执行工具
  6. 工具可以访问文件系统、Shell、网络,也可以通过 MCP 调用外部服务
  7. 工具结果回到 query 循环,触发下一轮迭代
  8. 最终的文本响应通过 Generator 的 yield 链回到 UI 层渲染

2.6 设计决策分析

为什么用 Generator 而不是 EventEmitter 或 RxJS?

Claude Code 完全可以用 EventEmitter(on('message', handler))或 RxJS(stream.pipe(map(...)).subscribe(...))来实现流式管道。但它选择了 Generator,原因在于:

控制反转的方向。 EventEmitter 是推模型——生产者主动推送事件,消费者被动接收。Generator 是拉模型——消费者主动拉取下一个值。在 Claude Code 的场景中,拉模型更合适:UI 渲染的速度决定了处理的速度,如果 UI 来不及渲染,Generator 自然暂停生产者,形成隐式的背压(backpressure)。

类型安全。 AsyncGenerator<StreamEvent | Message, Terminal> 的类型签名清楚地表达了:yield 产出的值的类型(StreamEvent | Message)和最终 return 值的类型(Terminal)。这比 EventEmitter 的字符串事件名或 RxJS 的复杂类型体操更直观。

生命周期管理。 Generator 的 return() 方法和 finally 块提供了确定性的资源清理机制。当用户中断操作时,for await...of 循环退出会自动触发 Generator 的清理逻辑,无需手动管理订阅的取消。

为什么工具系统没有使用类继承?

Tool 是一个 TypeScript 类型(接口),而不是一个抽象类。每个工具是一个普通对象或通过 buildTool() 工厂函数创建的对象,没有继承关系。

这个决策反映了组合优于继承的原则。工具之间的差异是在各个维度上独立变化的——有的工具自己定义了 isDestructive,有的沿用 buildTool() 给的默认值;有的工具是 MCP 工具(isMcp),有的是 LSP 工具(isLsp)。如果使用类继承,要么需要一个包含所有方法的庞大基类(大部分方法在大部分子类中是空实现),要么需要复杂的 mixin 组合。

使用接口+可选方法的模式,每个工具只需实现它实际需要的方法。buildTool() 工厂函数可以为缺失的可选方法提供默认实现,在保持灵活性的同时减少样板代码。

为什么权限检查在 React Hook 中?

useCanUseTool 是一个 React Hook,这看起来有些奇怪——权限检查不是纯逻辑吗?为什么要绑定到 React 的渲染循环?

原因在于权限确认可能需要 UI 交互。当静态规则无法判定时,系统需要向用户展示权限确认对话框,等待用户点击"允许"或"拒绝"。这个交互过程需要更新 UI 状态(显示对话框、更新确认队列),而 React 的状态管理是通过 Hook 驱动的。

useCanUseTool 接收 setToolUseConfirmQueue 和 setToolPermissionContext 作为参数,这使得权限确认的 UI 更新与组件的渲染周期对齐。同一个 Hook 内还按场景切换处理器:协调器模式走 handleCoordinatorPermission(先跑 Hook 和分类器,再回退到对话框),swarm worker 走 handleSwarmWorkerPermission(先试分类器,不行就经邮箱转交 leader)。完全无头、无法弹框的场景,则由 hasPermissionsToUseTool 末尾的 shouldAvoidPermissionPrompts 分支处理(第 9 章)。

为什么启动阶段有副作用 import?

main.tsx 开头的三个带副作用的 import 违反了通常的 "import 应该是无副作用的" 原则。代码中甚至有专门的 ESLint disable 注释:

// eslint-disable-next-line custom-rules/no-top-level-side-effects
startMdmRawRead();

这是一个有意识的权衡。这些 import 副作用执行的是 I/O 操作(进程启动、Keychain 读取),尽早发起后,它们的执行时间与 main() 前段的工作重叠(ES 模块 import 会提升,重叠的不是 import 链本身,见第 3 章 §3.1.2)。如果等到真正要用时再同步调用,这些 I/O 就会串在关键路径上——按第 3 章引用的源码注释,仅 macOS 上两次 Keychain 读取就约 65ms。在 CLI 工具中,启动延迟是用户体验的关键指标,这个权衡是值得的。

代码中的注释非常详细地解释了每个副作用的原因和时序关系,这是一种负责任的做法——当你必须违反常规时,确保后来者能理解你为什么这样做。

2.7 本章小结

本章我们完成了对 Claude Code 架构的全景巡览。让我们回顾关键要点:

一条消息的旅程 揭示了系统的运行时行为:从 CLI 入口,经过消息标准化、系统提示词组装、query 循环、API 调用、工具编排、权限检查、工具执行,最终回到终端。这不是线性的流水线,而是一个可能多次迭代的循环——每次工具调用都会触发新一轮 API 请求,直到模型认为任务完成。

六大子系统 构成了系统的静态骨架:CLI 入口负责启动和模式分流,Query 引擎管理会话状态,工具系统提供可扩展的能力抽象,权限系统保障安全边界,MCP 集成连接外部世界,Bridge(Remote Control)让本机会话可以从远端接管。

五个架构模式 是理解代码组织的钥匙:Generator 流式管道使数据在各层间自然流动,工具自描述对象使系统可扩展而无需修改编排层,权限上下文栈把多源规则汇总后按 deny → ask → allow 判定,特性标志与死代码消除在一套代码库中支撑了多种构建产物,依赖注入让 query 循环可以脱离真实 API 做测试。

理解了架构全景之后,你应该已经对 Claude Code 的整体设计有了清晰的心智模型。当你在后续章节中深入某个子系统时,可以随时回到本章的全景图来定位自己的位置,理解当前子系统与其他部分的关系。

在接下来的章节中,我们将深入每个子系统的内部实现。第3章将聚焦 CLI 启动过程的细节——从进程启动到第一个用户提示符出现之间的每一个步骤。第4章将深入 Query 引擎的循环机制,包括上下文窗口管理的多种策略。第5章将解析流式响应的处理管道。每一章都会像本章跟踪消息旅程那样,以具体的代码路径为线索,揭示设计决策背后的工程思考。

建议读者在阅读后续章节时保持源码编辑器打开,对照本章给出的文件路径自行探索。阅读源码不是线性的——你会不断在文件之间跳转,而本章建立的导航地图正是为此而设。