Claude Code 源码深度解析

第1章 为什么需要理解 Claude Code

作者 杨艺韬 · 8,255 字

1.1 AI 编程助手的演进

从 GitHub Copilot 的单行补全,到 Cursor 的多文件编辑,再到 Claude Code 的完全自主 Agent 模式——AI 编程助手在短短三年内经历了三次范式跃迁。

每一次跃迁的背后,不只是模型能力的提升,更是系统架构的根本性变化:

阶段 代表产品 架构模式 核心挑战
补全式 Copilot 请求-响应 上下文窗口有限
对话式 Cursor, Windsurf 多轮对话 + 工具调用 工具编排、权限控制
Agent 式 Claude Code 自主循环 + 多 Agent 安全边界、状态管理、任务分解

Claude Code 代表的是第三阶段:模型不再只是"建议者",而是"执行者"。它可以自主决定读哪个文件、执行什么命令、创建什么分支,甚至可以生成子 Agent 并行处理任务。

graph LR
    subgraph "第一阶段: 补全"
        C1["用户写代码"] --> C2["模型补全一行"]
    end
    subgraph "第二阶段: 对话"
        D1["用户描述需求"] --> D2["模型生成代码"] --> D3["用户确认/修改"]
    end
    subgraph "第三阶段: Agent"
        A1["用户描述目标"] --> A2["Agent 自主规划"]
        A2 --> A3["读代码 / 搜索"]
        A3 --> A4["编辑 / 执行"]
        A4 --> A5["验证 / 迭代"]
        A5 -->|"未完成"| A3
    end
    style C1 fill:#dbeafe,stroke:#3b82f6
    style D1 fill:#fef3c7,stroke:#f59e0b
    style A1 fill:#dcfce7,stroke:#22c55e

这种能力的实现,需要一套远比传统 CLI 工具复杂得多的架构。

Claude Code 的规模

让我们用数字感受一下这个系统的规模:

源码总量:        ~510,000 行 TypeScript
内置工具:        40+ 个(Read, Write, Edit, Bash, Grep, Glob, Agent...)
CLI 命令:        80+ 个
MCP 协议集成:    src/services/mcp/ 约 25 个文件、40 万字符(客户端 + OAuth + 服务发现,详见第 11 章)
权限规则引擎:    5 种模式 + 动态分类器(详见第 9 章)
终端 UI:         React + Ink 组件库(详见第 17 章)

作为对比:一个典型的"30 行 Agent Demo"只有模型调用 + 工具循环。Claude Code 在这之上多出的 51 万行代码,就是 Harness——让模型变成可靠产品的一切工程。

1.2 为什么选择 Claude Code 作为学习对象

市面上有很多 AI 编程工具,为什么要深入 Claude Code?

第一,它的源码是完整可见的。 Cursor、Copilot、Windsurf 的核心代码都是闭源的。2026 年 Claude Code 的完整源码通过 npm source map 泄露,这是第一次有生产级 AI 编程助手的内部实现被完整披露,你可以看到每一行实现。

第二,它的架构足够复杂。 51 万行代码不是堆砌出来的——工具系统、权限模型、MCP 集成、多 Agent 协调、IDE Bridge,每个子系统都经过了深思熟虑的设计。这些设计模式可以直接迁移到你自己的 Agent 项目中。

第三,它在真实生产环境中经受了考验。 这不是一个 demo 或 POC,而是被数百万开发者日常使用的工具。每一个"看起来奇怪"的设计决策背后,通常都有一个真实的 bug 或性能问题驱动。

1.3 你将学到什么

读完本专栏,你将理解:

系统架构层面

  • 如何设计一个流式 Agent 循环,让 AI 的思考过程实时可见
  • 如何构建可扩展的工具系统,支持 40+ 工具的注册、校验、执行和权限控制
  • 如何实现多模式权限模型,在安全和效率之间找到平衡点
  • 如何集成 MCP 协议,让第三方工具无缝接入

工程实践层面

  • 如何用 async generator 实现可组合、可取消的流式管道
  • 如何用 Zod 做运行时类型校验,让工具输入类型安全
  • 如何用 React + Ink 构建复杂的终端 UI
  • 如何通过并行预加载特性标志优化 CLI 启动速度
  • 如何用 JWT 实现 CLI 与 IDE 之间的安全通信

设计思维层面

  • 为什么选择"工具即对象"而不是类继承?
  • 为什么用 generator 而不是回调?
  • 为什么权限检查要分静态规则和动态分类器两层?
  • 为什么 MCP 客户端要实现完整的 OAuth 流程?

这些问题的答案,比代码本身更有价值。

1.4 本专栏的组织方式

本专栏按照从外到内、从宏观到微观的顺序组织:

  1. 架构总览(第2章)——先看全景图,理解各子系统的关系
  2. 启动与核心循环(第3-5章)——从 CLI 入口到 Agent 循环,理解主干流程
  3. 工具系统(第6-8章)——深入最核心的工具类型、编排和实现
  4. 权限与安全(第9-10章)——理解安全模型的设计哲学
  5. 协议与集成(第11-13章)——MCP、IDE Bridge、LSP 的集成架构
  6. Agent 进阶(第14-16章)——多 Agent、Skill、上下文管理
  7. UI 与工程(第17-18章)——终端 UI 和可迁移的设计模式

每一章的结构是:设计意图 → 代码实现 → 可迁移的模式

让我们开始吧。

1.5 一窥源码——Tool 类型的 30 多个字段

为了让"51 万行代码"不是抽象数字——给你一个具体切片:Claude Code 的 Tool 类型(src/Tool.ts:362+)定义了30 多个字段和方法——每一个都对应一类真实产品需求。下面按职责分组列出其中的主要部分(省略少量内部字段):

export type Tool<Input, Output, P> = {
  // 身份相关
  readonly name: string
  aliases?: string[]                              // 重命名兼容
  readonly inputSchema: Input                     // Zod schema
  readonly inputJSONSchema?: ToolInputJSONSchema  // MCP 原生 JSON Schema
  searchHint?: string                             // ToolSearch 关键词

  // 执行相关
  call(args, context, canUseTool, parent, onProgress): Promise<ToolResult>
  description(input, options): Promise<string>    // 动态描述、每次调用生成
  validateInput?(input, context): Promise<ValidationResult>
  checkPermissions(input, context): Promise<PermissionResult>

  // 能力声明
  isConcurrencySafe(input): boolean              // 能和其他工具并发吗
  isReadOnly(input): boolean                      // 是读操作吗
  isDestructive?(input): boolean                  // 不可逆吗
  isOpenWorld?(input): boolean                    // 访问外部系统吗
  isSearchOrReadCommand?(input): {...}           // UI 折叠展示
  isEnabled(): boolean
  isMcp?: boolean                                 // 来自 MCP server
  isLsp?: boolean                                 // 来自 LSP

  // UI 和交互
  interruptBehavior?(): 'cancel' | 'block'       // 被打断时的行为
  requiresUserInteraction?(): boolean

  // 性能和 token
  maxResultSizeChars: number                     // 结果持久化门槛
  readonly strict?: boolean                       // API 严格模式
  readonly shouldDefer?: boolean                  // 延迟加载 schema
  readonly alwaysLoad?: boolean                   // 总是加载

  // 生命周期 hook
  backfillObservableInput?(input): void          // observer 看到前修饰
  inputsEquivalent?(a, b): boolean               // 等价性判断

  // MCP 元信息
  mcpInfo?: { serverName; toolName }
  outputSchema?: z.ZodType<unknown>

  // 文件路径
  getPath?(input): string

  // hook 准备
  prepareMatcher?(rule: string): (input) => boolean
}

每个字段都 answer 一个具体的工程问题

  • 没有 isConcurrencySafe——并发执行两个 Edit 会 race condition
  • 没有 interruptBehavior——用户 Ctrl-C 后长命令依然跑到完成
  • 没有 shouldDefer——所有工具 schema 全塞 prompt、token 爆掉
  • 没有 backfillObservableInput——hook 看不到 legacy 字段
  • 没有 checkPermissions——所有工具都能无条件执行、安全崩
  • 没有 prepareMatcher——每次 hook 判断都重新 parse 规则、性能崩

一个字段 = 一类真实产品问题的沉淀。这是本专栏第一个 takeaway——简单的"工具"概念在生产级 Agent 系统里变成了三十来维的空间。学会识别这些维度,你就知道评估任何 Agent 框架时要问什么问题。

1.6 TypeScript 类型系统作为架构语言

注意 Tool 类型的签名:

export type Tool<
  Input extends AnyObject = AnyObject,
  Output = unknown,
  P extends ToolProgressData = ToolProgressData,
> = { ... }

三个泛型参数——让每个工具精确表达自己的输入、输出、进度类型。这不是装饰——它让 TypeScript 编译器在工具调用处做静态类型检查

// FileReadTool 的 call 接受 { file_path: string, offset?: number, limit?: number }
// FileEditTool 的 call 接受 { file_path: string, old_string: string, new_string: string }
// 类型错了编译期就挂掉

为什么不用 any 省事?——因为 Claude Code 内部有 40+ 工具——手动记住每个的参数不现实。TypeScript 泛型让 IDE 自动补全、重构一处改 30 处——开发效率和正确性双赢

这是 TypeScript 作为"架构语言"的核心价值——不只是 JS + 类型检查、是让代码的架构意图能被编译器理解。Claude Code 的 51 万行里、类型定义占了可观比例——这些类型是活的文档、也是编译期防御。

对比 Python agent 框架(没有强类型)——每个工具的参数要读文档或 docstring 才知道——LLM 输出错了、要运行时才能发现。TypeScript + Zod 让这些问题提前到编译期——这对 51 万行代码的可维护性是决定性的。

1.7 从 30 字段看产品级 vs demo 级的差距

网上有大量 "30 行代码写一个 Claude Code-like agent" 的教程——它们长这样:

# 30 行版
TOOLS = {
    "read_file": lambda path: open(path).read(),
    "write_file": lambda path, content: open(path, "w").write(content),
    "bash": lambda cmd: subprocess.run(cmd, shell=True).stdout.decode(),
}

def agent(prompt):
    while True:
        response = llm.call(prompt, tools=TOOLS)
        if response.is_final:
            return response.text
        result = TOOLS[response.tool_name](**response.tool_args)
        prompt += f"\nTool result: {result}"

这 30 行能跑——对 hello-world 够用。但生产级 Claude Code 的 51 万行包含的每一块都对应 demo 里的一个失败模式:

  • Demo 失败bash("rm -rf /") ——Claude Code 的 checkPermissions
  • Demo 失败:并发 write_file 到同一路径 race ——Claude Code 的 isConcurrencySafe
  • Demo 失败:read 一个 10GB 文件内存爆 ——Claude Code 的 maxResultSizeChars 持久化到磁盘
  • Demo 失败:用户 Ctrl-C 长命令继续跑 ——Claude Code 的 interruptBehavior 中止
  • Demo 失败:每次 prompt 重传 40 工具 schema、token 爆 ——Claude Code 的 shouldDefer 延迟加载
  • Demo 失败:LLM 写错工具名叫 old name ——Claude Code 的 aliases 兼容
  • Demo 失败:第三方 MCP server 注入恶意工具 ——Claude Code 的 isMcp + OAuth 隔离
  • Demo 失败:tool 输出污染下次 prompt ——Claude Code 的 backfillObservableInput + hook 控制

一百个小问题 × 真实生产的放大系数 = 51 万行。这就是为什么这个专栏要花几百页讲 Claude Code——每一万行都解决一类你在 demo 里想不到的问题

读这个专栏的回报是什么?——下次你自己写 Agent 时、知道要考虑这些维度——不用等到生产环境被用户发现 bug 再回炉。

1.8 Harness 这个词的由来

本专栏反复用到 "Harness"(挽具)这个词——它是 Anthropic 社区对 "让模型真的能用起来的所有工程代码" 的惯用说法。

词源自 Harness horse——马的挽具、让马的原始力量能被有意图地用。LLM 模型是"马"、Harness 是"让马能拉车"的所有周边工程

  • Prompt engineering——怎么写系统提示、few-shot 例子
  • Tool orchestration——40+ 工具的注册、权限、并发
  • Memory / Context——对话历史、工作区状态的管理
  • Safety rails——什么能做、什么不能做
  • Error recovery——出错怎么 retry、怎么降级
  • Observability——日志、metrics、用户可见性
  • UX layer——终端 UI、流式输出、中断响应

Harness 是比"prompt"更深的功夫——prompt engineering 工程师多、Harness engineering 工程师少。Anthropic 自己内部 Claude Code 团队称自己是 "harness team"——这暗示了整个产品的技术密度。

读 Claude Code 源码、就是学世界顶级的 harness engineering。

1.9 和本系列其他专栏的关联

这个专栏是 15 个系列专栏中的一个——和其他几个有紧密关联:

  • 《MCP 协议设计与实现》——Claude Code 内置完整的 MCP 客户端(src/services/mcp/,约 25 个文件、40 万字符),本专栏第 11 章会反复引用 MCP 书的协议设计
  • 《LangChain 设计与实现》——LangChain 的 Agent 概念是 Claude Code 早期灵感之一、本专栏第 2 章会对比两者的设计哲学
  • 《LangGraph 设计与实现》——LangGraph 的 Pregel 模型和 Claude Code 的 Agent 循环是两种不同的 agent runtime 范式——本专栏第 4 章会深入对比
  • 《React 19 内核探秘》——Claude Code 用 React + Ink 做终端 UI、本专栏第 17 章的内容直接建立在 React 源码理解上
  • 《Serde 元编程》——Claude Code 大量用 Zod 做 schema validation、和 Serde 的 derive 机制同构——本专栏第 6 章会关联对比

读这几个专栏合起来、才能完整把握 "Agent 工程" 这个新兴领域的工程全景

1.10 阅读建议

这个专栏不是 Claude Code 的使用手册——想学怎么用、看官方文档。这个专栏是源码内幕——假设读者:

  • 用过 Claude Code 至少几次、理解基本概念(对话、工具调用、权限)
  • 懂 TypeScript(或至少 JavaScript + 基本类型概念)
  • 有一定的架构直觉(至少写过几千行代码的项目)

如果你是前端工程师——React + Ink + async 部分会很顺、工具系统部分需要多点想象。

如果你是后端工程师——工具系统 / 权限 / 协议部分会很顺、前端 UI 部分可以快读。

如果你是 AI 工程师但没大项目经验——Harness 部分是核心财富、会补齐你从 "会 prompt" 到 "能写生产 agent" 的关键一跃。

读完这一章、你应该对 "Claude Code 到底是什么"、"为什么复杂"、"学它有什么价值" 有了一个骨架认识。下一章进入架构全景——从入口到循环到工具、把整个系统串成一张图——让你在深入任何细节前、先有全局视角。

1.11 一个真实对话的全链路——窥探系统内部

用一个简单的对话示例——"请找出项目里所有待办注释"——展开看 Claude Code 内部发生的事:

Step 1:用户输入进入终端——React + Ink 组件接收 keystroke、累积到 prompt buffer。按 Enter 后 prompt 被 serialize、送到 main query loop。

Step 2:QueryEngine 启动src/QueryEngine.ts,1295 行、巨石一块)——拼装 system prompt、user message、注入工具 schema(部分 defer-loaded)、调用 Anthropic API。

Step 3:LLM 响应流开始——Server-Sent Events 一个 token 一个 token 进来。React UI 通过 async generator 实时渲染——用户看到"打字机效果"

Step 4:LLM 输出 tool_use block——"I'll use Grep to search task markers"——同时输出 tool call:grep("FIXME|待办", { include: "*.{ts,tsx}" })

Step 5:tool_use block 触发 QueryEngine 的 tool dispatch——找到 GrepTool、调用 validateInputcheckPermissionscall

Step 6:GrepTool 的 call 方法——用 ripgrep 系统调用、流式返回匹配行。每批匹配通过 onProgress 推到 UI——用户看到搜索进度

Step 7:tool_result 回填——GrepTool 执行完毕、结果按 maxResultSizeChars 判断——小的直接回给 LLM、大的持久化到磁盘再把文件路径回给 LLM。

Step 8:LLM 拿到 tool_result 继续生成——可能继续调 Read 查看几个匹配、继续调 Grep 缩范围、或者直接生成最终总结。

Step 9:LLM 输出 final text——回到 React UI 流式渲染——用户看到 "找到 47 个待办标记、主要集中在 src/utils/... 和 tests/..."。

Step 10:对话留存——user message + assistant messages 都 append 到 conversation history、写到磁盘的 .claude/history/ 供下次会话恢复

这 10 步每一步都是 Claude Code 源码里的几百到几千行代码——整个 51 万行就是这 10 步的各种路径和边界情况。本专栏的 18 章就是把这 10 步拆开讲——每一步的设计动机、代码实现、工程权衡。

1.12 Claude Code 和传统 IDE 插件的边界

一个常见的比较——Claude Code vs GitHub Copilot / Cursor——三者都是 "AI 辅助编程"、但架构和定位完全不同:

Copilot(IDE 内嵌):

  • 触发:每次输入后自动触发
  • 输出:单行/多行补全、inline 展示
  • 交互:被动 ——你打字它补、你接受或拒绝
  • Tool:只有"补全代码" 这一个 tool
  • 架构复杂度:主要在补全 UI 和 ranking 算法

Cursor(IDE Fork):

  • 触发:对话框 + 文件选择器
  • 输出:多文件 diff
  • 交互:半主动——你描述需求、AI 产 diff、你 review
  • Tool:读文件、写文件、运行命令——基础但没到 Agent 级
  • 架构复杂度:编辑器集成 + 多文件上下文管理

Claude Code(终端 CLI):

  • 触发:显式命令claude
  • 输出:对话 + 自主工具调用
  • 交互:主动 ——你描述目标、AI 自主规划 + 执行、你监督
  • Tool:40+ 工具——读写、搜索、命令、权限、MCP 扩展、Agent 子进程
  • 架构复杂度:完整的 agent runtime + harness 全栈

三者服务不同的心智距离——Copilot 是 "加速你打字"、Cursor 是 "协助你编辑"、Claude Code 是 "替你执行任务"。距离越远、需要的 harness 越多——这就是 Claude Code 比另外两个复杂度高一个量级的根本原因。

选哪个?——看任务类型:

  • "补完这个函数"——Copilot 最快
  • "重构这个模块、改 5 个文件"——Cursor 最顺
  • "调研项目、找 bug、自动修复、写测试"——只有 Claude Code 能自主完成

三者是互补而非替代——真实开发者往往三个都用、按场景切换。

1.13 为什么讲 TypeScript 而不是 Go/Rust 实现

有读者可能问——Anthropic 为什么用 TypeScript 写 Claude Code、不用更"性能的" Go 或 Rust?

几个原因:

① 生态成熟度——LLM 生态的 SDK 以 Python 和 TypeScript 为主、Rust/Go 的 LLM SDK 是二等公民。Anthropic 自己的 SDK 用 TS 先发。

② 快速迭代——Claude Code 2024-2026 是产品高速迭代期——TypeScript 的类型推导 + 热重载比 Rust 编译时间友好得多。

③ UI 生态——React + Ink 让终端 UI 能用前端的组件思维——Rust 的 tui-rs / Ratatui 要自己手写布局、开发效率低 3-5 倍。

④ LLM 消费者的生态定位——Claude Code 的用户主要是前端 / 后端 / AI 工程师——TypeScript 是最大公约数开源后二次开发的门槛低

⑤ 性能不是瓶颈——Claude Code 的瓶颈是LLM API 延迟(秒级)、不是 JS 执行速度(毫秒级)。用 Rust 重写能省 10ms——但用户感知是 LLM 的 2 秒响应——优化 UI 状态比优化底层有意义

⑥ 招聘池——全球 TypeScript 开发者比 Rust 多 10×——维护团队扩容容易。

TypeScript 的劣势——运行时类型擦除、需要 Zod 做 runtime validationNode.js 内存占用高于 Rust 进程——但对这个产品形态,这些劣势可接受。

工具选择永远是 "用最适合场景的"——不是 trendy 的、也不是极限性能的、而是 "能快速交付优质产品的"。Anthropic 的选择给所有团队一个参考——追求产品速度时、别让 Rust 迷恋拖慢你。

1.14 Claude Code 源码目录结构全景

在进入后续章节前、扫一眼源码目录——让你对后续章节对应的文件位置有个地图。src/ 下面 53 个顶层目录/文件——按职责分组如下:

核心 runtime(~100k 行)

  • QueryEngine.ts(1295 行)——主查询循环、消息编排
  • Tool.ts(792 行)——工具类型定义
  • Task.ts(125 行)——后台任务管理
  • commands/commands.ts——slash commands 注册
  • hooks/——各类 pre/post hook 的协议
  • context/——对话上下文、消息流
  • coordinator/——多 agent 协调

工具实现(~150k 行)

  • tools/(40+ 子目录)——每个工具独立文件夹
  • services/mcp/——MCP 协议客户端实现
  • services/lsp/——LSP 客户端(第 12 章)

UI 层(~50k 行)

  • components/——React + Ink 组件
  • ink/ink.tsx——Ink 集成
  • dialogLaunchers.tsx——对话框触发器
  • keybindings/——快捷键系统

入口和配置(~30k 行)

  • entrypoints/——不同入口(cli、sdk、bridge)
  • cli/——CLI 解析和路由
  • bootstrap/——启动前的预加载和初始化
  • constants/——常量定义

持久化和迁移(~20k 行)

  • memdir/——会话记忆目录
  • history.ts——对话历史
  • migrations/——老版本数据迁移

bridge 和集成(~40k 行)

  • bridge/——和 IDE 的桥接协议
  • native-ts/——原生 TypeScript 集成
  • assistant/——assistant 配置和 profile

其他(~120k 行)

  • buddy/——另一种 agent 模式(第 14 章)
  • cost-tracker.tscostHook.ts——成本追踪
  • interactiveHelpers.tsx——交互辅助

53 个顶层条目映射到本专栏的 18 章——每一章深入 1-3 个模块。你在读某一章时、可以直接翻到对应源码文件对照看——是本专栏的核心学习方式。

1.15 源码可见性的来源

这个专栏的契机是 2026 年 Claude Code 完整源码通过 npm source map 泄露。npm 默认发布的 .js.map 文件包含原始 TypeScript 源码作为 sourceContent —— 用 source-map-explorer 类工具几分钟就能 reverse 回 .ts

为什么没 strip source map?推测原因:

  • Strip 后 debugging 体验受损:用户报告 bug 时栈信息变乱
  • Minify 后的 JS stack trace 对用户无意义,map 是关键
  • Product velocity 优先于代码隐私:每天发多版,strip 流程会拖慢

这种意外公开的边界很灰——source map 本身是默认公开的 artifact,但 Anthropic 没有显式同意它被系统性研究。本专栏的处理方式是:只讲设计原则、不发布完整代码、引用最小必要片段,用于教学而非竞品复制。

1.16 术语约定

全专栏会用到一些特定术语——先统一约定、避免混淆:

  • Agent——指 Claude Code 的某次运行实例(类似"进程")
  • Sub-agent——指 AgentTool 启动的子进程(第 14 章)
  • Tool——指 Claude Code 的工具实现(区别于"工具"泛指)
  • Session——指一次完整的对话会话(磁盘持久化)
  • Harness——让模型变成可靠产品的所有工程
  • Turn——一次 user → assistant 的来回(对话的基本单元)
  • Stream——LLM 的 token-by-token 输出流
  • Hook——插入到特定生命周期点的扩展代码
  • MCP——Model Context Protocol、第三方工具协议

这些术语在第 2 章会扩充,每个术语后都有精确定义。

1.17 三个真实数字——感受代码的密度

再给你三个数字感受 Claude Code 的工程密度

数字一:Tool.ts 单文件 792 行、30 多个字段和方法——这只是类型定义、不是实现。真实工具实现在 40 个子目录里、每个 500-3000 行——光 FileEditTool 的实现就 ~2000 行、因为要处理:

  • 文件不存在的 create
  • 文件已存在的 edit
  • 多 replace 的 batch
  • LSP 诊断集成
  • git rename detection
  • 权限/hook 调用
  • UI diff 渲染
  • 错误恢复
  • 测试 mock 接口

FileEdit 这一个工具的复杂度、就超过你想象的"用 fs.writeFile 一行搞定"几十倍。每个工具都如此。不过这里要给个实测数字而不是拍脑袋:作者同一份快照下 src/tools/ 合计 42309 行(统计口径见《Harness Engineering》第 1 章 §1.3.1),约 40 个内置工具,平均每个工具约 1057 行——包含 prompt、实现、类型与测试。单看 BashTool 就有 10894 行、PowerShellTool 7829 行,而 FileReadTool 只有 1418 行:工具之间的体量差着近十倍,"平均值"在这里意义有限,真正说明问题的是最重的那几个。

数字二:80+ CLI 命令——包括 /help/clear/compact/status/review/fast/worktree/schedule/sidebar/logout/debug 等。每个命令后面都有完整的 React 组件、slash 解析、参数校验——平均每个 300 行、合计约 24k 行

数字三:40+ 键盘快捷键和 shortcuts——按键组合 + 上下文映射——支持 Vim 模式、Emacs 模式、默认模式——三种模式下同一键位可能有完全不同含义——键绑定系统本身就是一个小型状态机

这三个数字撑起的是功能面的广度,不要拿它们去反推总行数——工具、命令、快捷键之间大量共享 utilities,简单相乘会重复计算。全仓规模以本专栏前言给出的快照口径为准。

这个量级的代码量在 CLI 工具里是极端罕见的——Git 的核心约 200 万行(主要 C)、VSCode 的核心约 1300 万行(TS)——Claude Code 作为一个不到 2 年的项目51 万行是惊人的工程产出密度

1.18 第 2 章的骨架图预告

下一章会给出一张一屏图——把 Claude Code 整个系统画成这样:

    ┌──────────────────────────────────────────────────┐
    │  CLI entry (claude)                              │
    │    ↓                                             │
    │  Bootstrap (parallel preload, feature flags)     │
    │    ↓                                             │
    │  QueryEngine (main loop)                         │
    │    ↓    ↑                                        │
    │  ┌──────┴──────┐                                 │
    │  │ LLM API     │  ←→  Streaming (SSE)            │
    │  └──────┬──────┘                                 │
    │    ↓                                             │
    │  Tool Dispatch                                   │
    │    ↓                                             │
    │  ┌──────────────────────┐                        │
    │  │ validateInput()      │                        │
    │  │ checkPermissions()   │  ←→  Permission Engine │
    │  │ Hooks (pre/post)     │                        │
    │  │ call()               │                        │
    │  └──────────────────────┘                        │
    │    ↓                                             │
    │  Tool Result                                     │
    │    ↓                                             │
    │  React + Ink UI (streaming)                      │
    │    ↓                                             │
    │  History / Persistence                           │
    └──────────────────────────────────────────────────┘

这张图的每一条线都对应一章——18 章读完、这张图就从简化的 box-and-arrow 变成带代码记忆的真实系统。

1.19 代码阅读方法论

分享一套读复杂 TypeScript 项目的方法论——在后续章节反复用到:

① 先读 type 定义、再读 implementation——Tool.ts 的 792 行里一半是类型定义——看懂这些类型就懂了 80% 的设计意图。实现只是填充类型骨架的血肉

② 入口优先——先看 main.tsx / cli/ 入口——顺着调用链下沉——比随机翻某个文件理解得快 10 倍。

③ 找 central hub——QueryEngine / Tool / Task 这种被所有地方引用的核心类型——是理解整体的锚点。

④ 按"数据流"读、不是按"调用栈"读——跟踪用户输入怎么变成 LLM 响应工具调用怎么回传结果——比读每个函数的调用关系顺得多。

⑤ 大量用 grep / search——找一个函数在哪被调——ripgrep + context lines——几秒知道一个函数的生命周期。

⑥ 画图同步——一边读一边在纸上/白板画 box-and-arrow——主动构造心智模型——比单纯看代码记住多 5 倍。

⑦ 对比类比——遇到新概念问自己 "像别的什么"——比如 Tool 的 30 字段类似于Rust trait 的多 method、或Java Servlet 的 lifecycle methods——类比能快速建立理解。

⑧ 不懂就跳过——第一遍读完整个目录允许 30% 不懂——第二遍重点攻这 30%。线性读完一遍 > 前 20% 读透但永远没读后面——先拿到整体感最关键。

这套方法论在读 React / Tokio / serde 等复杂项目时同样适用。

1.20 一段真实源码——ToolPermissionContext

在结束前、再给你一段真实源码切片感受 Claude Code 的密度——Tool.ts 里的 ToolPermissionContext

export type ToolPermissionContext = DeepImmutable<{
  mode: PermissionMode
  additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>
  alwaysAllowRules: ToolPermissionRulesBySource
  alwaysDenyRules: ToolPermissionRulesBySource
  alwaysAskRules: ToolPermissionRulesBySource
  isBypassPermissionsModeAvailable: boolean
  isAutoModeAvailable?: boolean
  strippedDangerousRules?: ToolPermissionRulesBySource
  /** When true, permission prompts are auto-denied
   *  (e.g., background agents that can't show UI) */
  shouldAvoidPermissionPrompts?: boolean
  /** When true, automated checks (classifier, hooks) are awaited before
   *  showing the permission dialog (coordinator workers) */
  awaitAutomatedChecksBeforeDialog?: boolean
  /** Stores the permission mode before model-initiated plan mode entry,
   *  so it can be restored on exit */
  prePlanMode?: PermissionMode
}>

短短 25 行代码、至少藏着 8 个深层工程决策

DeepImmutable<{...}> wrapper——让整个类型递归只读(包括 Map、包括嵌套对象)——防止中间层代码 mutate context。这比 Rust 的 Arc<T> 还严格——TypeScript 类型层面就禁止改动。

Map<string, AdditionalWorkingDirectory> 而非 Record<string, ...>——Map 保持插入顺序、允许非字符串 key(未来扩展)、iteration 更清晰——Record 是 plain object、有 prototype pollution 风险。

③ 三种规则分类 alwaysAllow/Deny/Ask——对应 三种权限决策——比"allow/deny"两态丰富、让"ask once then remember" 这类交互成为可能

ToolPermissionRulesBySource——权限规则按来源分组(workspace 配置、用户全局配置、CLI flag 等)——让用户知道哪条规则来自哪里、易于 debug。

isBypassPermissionsModeAvailable——运行时 feature flag——某些环境(企业部署)禁用 bypass 模式——让权限系统有 defense-in-depth。

shouldAvoidPermissionPrompts——区分 "可以弹 UI 问用户" vs "后台 agent 不能问"——后台 agent 看到权限问题只能拒绝、不能阻塞等用户。

awaitAutomatedChecksBeforeDialog——coordinator workers 的特殊需要——先跑 classifier + hook、再决定要不要问用户——让 dialog 只出现在真正需要人决策的场景、减少打扰。

prePlanMode——Plan mode 进出的状态保存——从 plan mode 出来后能回到进入前的状态——避免用户丢失上下文

25 行代码 = 8 个产品决策 = 多年用户反馈的沉淀。这就是本专栏要拆解的东西——让你看到代码之下的产品心智。

1.21 searchHint 字段——ToolSearch 机制的开端

Tool 类型里有一个小字段、承载着 Claude Code 最精妙的"token 管理策略"——searchHint

/**
 * One-line capability phrase used by ToolSearch for keyword matching.
 * Helps the model find this tool via keyword search when it's deferred.
 * 3–10 words, no trailing period.
 * Prefer terms not already in the tool name (e.g. 'jupyter' for NotebookEdit).
 */
searchHint?: string

这个字段存在的原因——Claude Code 的40+ 工具如果 schema 全塞 prompt、几千 token 都不够——每次调用都浪费 context window。

解法ToolSearch 机制——把一部分工具标记为 deferred、它们的 schema 不进初始 prompt——LLM 要用时调 ToolSearch tool 搜索、搜到后才加载 schema

searchHint 就是供 ToolSearch 做关键词匹配的——每个工具写 3-10 个关键词、让 LLM 能 "I need to edit a jupyter notebook" → ToolSearch 匹配到 NotebookEdit。

两个细节约束

  • 3–10 words——太短关键词不够、太长 schema 发送时反而更贵
  • "no trailing period"——避免拼接时出现 "jupyter. use for..." 的标点问题
  • "Prefer terms not already in the tool name"——别重复工具名本身、扩展搜索命中率

这个策略的数字效果

  • 无 ToolSearch:40 工具 × 每工具 ~500 token = 20,000 token 的工具 schema
  • 有 ToolSearch:5 核心工具常驻 × 500 + 35 deferred × 20 token searchHint = 3,200 token
  • 节省 83% 工具相关 token

省下的 token 给对话历史 + 代码上下文,让 Claude Code 能在同样 context window 下处理更长的任务。

这个小字段背后是整个 token 经济学——40+ 工具、8-token 的 searchHint 规范、ToolSearch 的三阶段调用,合起来是 Claude Code 能轻松使用这么多工具而不炸 context window 的关键。本专栏第 8 章会完整讲 ToolSearch 机制。

1.22 maxResultSizeChars 字段——输出爆炸的防护

再看一个字段——maxResultSizeChars

/**
 * Maximum size in characters for tool result before it gets persisted to disk.
 * When exceeded, the result is saved to a file and Claude receives a preview
 * with the file path instead of the full content.
 *
 * Set to Infinity for tools whose output must never be persisted (e.g. Read,
 * where persisting creates a circular Read→file→Read loop and the tool
 * already self-bounds via its own limits).
 */
maxResultSizeChars: number

问题场景:Grep 搜索一个大代码库、匹配 10000 行——直接塞进 LLM prompt 会爆 context

解法output persistence——结果超过 maxResultSizeChars(典型 4000-8000)时、写到磁盘、只回给 LLM 一个预览 + 文件路径。LLM 想看完整输出、用 Read 工具读那个文件——按需加载。

Infinity 的特殊值——Read 工具的输出不能持久化——因为 "持久化 Read 的结果到文件、再 Read 那个文件" 会形成无限循环。Read 有自己的 offset/limit 机制自 self-bound。

这种"结果太大自动持久化、LLM 按需加载" 的模式是LLM agent 必备机制——否则一次 grep 能把整个 context 吃光。

实现细节——Claude Code 内部有 ContentReplacementState 机制(utils/toolResultStorage)——把持久化的内容替换成 placeholder、后续 LLM 可以 reference

这个机制的巧妙处——对 LLM 透明——LLM 看到的 "结果太大、存到 /tmp/xxx.txt" 就当作正常的工具输出、自然地继续 chain-of-thought——Read 一下就拿到详情

一个 number field + 几行持久化逻辑 = LLM agent 能处理任意大小输出的关键 trick。

1.23 interruptBehavior ——用户 Ctrl-C 的哲学

最后一个字段——interruptBehavior

/**
 * What should happen when the user submits a new message while this tool
 * is running.
 *
 * - `'cancel'` — stop the tool and discard its result
 * - `'block'`  — keep running; the new message waits
 *
 * Defaults to `'block'` when not implemented.
 */
interruptBehavior?(): 'cancel' | 'block'

问题场景:Bash 工具正在跑 npm test(10 分钟)、用户突然说 "算了先写个 README"——怎么办?

两种策略

  • cancel——立刻停 test、开始写 README——响应快、但 test 状态丢失
  • block——让 test 跑完、完了再处理新 message——state 不丢、但用户等太久

Claude Code 的选择让每个工具自己决定——默认 block、特定工具可以 override 成 cancel。

典型选择

  • Bash 工具——可能 cancel(比如 watch mode 的 bash)
  • FileEdit——block(编辑是原子的、半途停会留损坏文件)
  • Grep / Read——默认 block、但大部分执行很快、用户几乎感知不到
  • AgentTool(子 agent)——block、子 agent 的结果珍贵

这种 "允许工具自定义中断语义" 的设计比 "统一 cancel / 统一 block" 更合理——因为不同工具的重启代价不同。

用户视角——在终端按 Ctrl-C、期望 "应该立刻响应"、但 Claude Code 的工具可能选择 block——这看似违反用户预期。

Claude Code 的妥协——强 Ctrl-C(两次连按)强制 cancel 所有、一次只是 "优雅中断请求"、让当前工具自己选——两种级别的中断让用户能 fine-tune

这个字段的存在提醒我们——Agent 系统的中断语义比想象复杂——写一个 Agent 要想好"正在执行的工作被打断时该怎样"。

1.24 backfillObservableInput 字段的故事

最后再讲一个 Tool 里的细节——backfillObservableInput 字段:

/**
 * Called on copies of tool_use input before observers see it (SDK stream,
 * transcript, canUseTool, PreToolUse/PostToolUse hooks). Mutate in place
 * to add legacy/derived fields. Must be idempotent. The original API-bound
 * input is never mutated (preserves prompt cache). Not re-applied when a
 * hook/permission returns a fresh updatedInput — those own their shape.
 */
backfillObservableInput?(input: Record<string, unknown>): void

这个字段回答的问题——tool 的 input schema 升级了、老代码还传老字段、怎么 observer 看到完整信息?

场景:假设 FileEditTool 老版本接受 { path, old, new }、新版本改成 { file_path, old_string, new_string }Claude API 用新 schema、但有的 MCP 插件可能还传老字段

解法——backfillObservableInput 在 input 传给 observers 前填入 legacy 字段

backfillObservableInput(input) {
  if (!input.file_path && input.path) input.file_path = input.path
  if (!input.old_string && input.old) input.old_string = input.old
  if (!input.new_string && input.new) input.new_string = input.new
}

observer 们(hook、transcript、UI)看到的 input 既包含新字段也包含老字段——老代码依然能读到期望的字段、新代码也能读新字段

注意注释里的三个约束

  1. "Mutate in place"——直接改 input 对象、不返回新 object——省内存
  2. "Must be idempotent"——可能被多次调用、第二次调用不该产生不同结果
  3. "The original API-bound input is never mutated (preserves prompt cache)"——API 发给 Anthropic 的 input 不被 mutate——保持 prompt 的缓存 hash 稳定

第 3 点最妙——Anthropic API 对相同 prompt 有缓存(节省 token 成本)——如果每次请求时 input 里都 backfill 新字段、prompt hash 会变、缓存失效。所以 backfillObservableInput 只对 observer 副本调用、不对 API 副本调用——既兼容观察者、又保缓存

"一个 input 两份副本、分别服务不同消费者"的架构是生产 AI 产品的常见优化——cache hit 能让成本降低 5-10×。

这个字段的存在体现了 Anthropic 对 token 成本的执着:每一个可能影响缓存的代码路径都经过精心设计。下一章进入架构全景图。