Claude Code 源码深度解析
第1章 为什么需要理解 Claude Code
1.1 AI 编程助手的演进
从 GitHub Copilot 的单行补全(2021 年 6 月技术预览),到 Cursor 的多文件编辑,再到 Claude Code 的自主 Agent 模式——AI 编程助手在短短几年内经历了几次范式跃迁。
每一次跃迁的背后,不只是模型能力的提升,更是系统架构的根本性变化:
| 阶段 | 代表产品 | 架构模式 | 核心挑战 |
|---|---|---|---|
| 补全式 | Copilot | 请求-响应 | 上下文窗口有限 |
| 对话式 | Cursor, Windsurf | 多轮对话 + 工具调用 | 工具编排、权限控制 |
| Agent 式 | Claude Code | 自主循环 + 多 Agent | 安全边界、状态管理、任务分解 |
Claude Code 代表的是第三阶段:模型不再只是"建议者",而是"执行者"。它可以自主决定读哪个文件、执行什么命令、创建什么分支,甚至可以生成子 Agent 并行处理任务。
注意这张表按的是各产品起步时的形态,不是它们今天的能力边界:Copilot 与 Cursor 后来都有了 Agent 模式(见 §1.12)。Claude Code 自己的时间线(均以 Anthropic 官方公告为准,核对于 2026-09-24):2025-02-24 随 Claude 3.7 Sonnet 以"有限研究预览"发布,当时就是终端工具;2025-05-22 随 Claude 4 宣布正式可用(GA),同时推出 VS Code / JetBrains 的 beta 扩展和 Claude Code SDK;2025-09-29 Claude Code SDK 更名为 Claude Agent SDK,官方说法是"驱动 Claude Code 的那套 agent harness"也能驱动其他类型的 Agent。截至核对日,官方文档列出的使用入口有终端、IDE 扩展、桌面应用(Code 标签页,自带 Claude Code,无需另装 CLI)和网页,并称各入口连接的是"同一个底层 Claude Code 引擎"。本专栏讲的是其中终端 CLI 那一份代码在 2026-03-31 前后的快照。
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 章)
权限规则引擎: 7 种模式(6 种用户可选 + 仅内部使用的 bubble)+ 动态分类器(详见第 9 章)
终端 UI: React + Ink 组件库(详见第 17 章)
口径说明:这里的 ~510,000 行是整仓、含测试的口径。如果只统计 src/ 一棵子树下的 .ts 文件(同样含测试),实测是 379997 行——这是《Harness Engineering》专栏第 1 章 §1.3.1 与《MCP 协议设计与实现》专栏第 19 章使用的口径。两个数不矛盾,差额来自 src/ 之外的部分与 .tsx 文件,但引用时不能互换。本专栏后文凡出现"51 万行",指的都是整仓口径。
作为对比:一个典型的"30 行 Agent Demo"只有模型调用 + 工具循环。Claude Code 在这之上多出的 51 万行代码,就是 Harness——让模型变成可靠产品的一切工程。
1.2 为什么选择 Claude Code 作为学习对象
市面上有很多 AI 编程工具,为什么要深入 Claude Code?
第一,它的源码曾经完整可见。 Claude Code 本身不开源(npm 包的 LICENSE.md 写的是 "© Anthropic PBC. All rights reserved."),Cursor、Windsurf 也是闭源产品。2026 年 3 月底 Claude Code 的完整 TypeScript 源码通过 npm 包里的 source map 意外流出,一个闭源的商业编程 Agent 的内部实现因此被外界看到。开源的同类并非没有——OpenAI 的 Codex CLI 与 Google 的 Gemini CLI 都以 Apache-2.0 开源,Aider 也是开源项目,Copilot 的 VS Code 聊天扩展 2025 年起以 MIT 许可开源——但 Claude Code 这份快照的规模与产品化程度,让它成了一个难得的解剖对象。
第二,它的架构足够复杂。 51 万行代码不是堆砌出来的——工具系统、权限模型、MCP 集成、多 Agent 协调、Remote Control 桥接,每个子系统都经过了深思熟虑的设计。这些设计模式可以直接迁移到你自己的 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 本专栏的组织方式
本专栏按照从外到内、从宏观到微观的顺序组织:
- 架构总览(第2章)——先看全景图,理解各子系统的关系
- 启动与核心循环(第3-5章)——从 CLI 入口到 Agent 循环,理解主干流程
- 工具系统(第6-8章)——深入最核心的工具类型、编排和实现
- 权限与安全(第9-10章)——理解安全模型的设计哲学
- 协议与集成(第11-13章)——MCP、Remote Control 桥接、LSP 的集成架构
- Agent 进阶(第14-16章)——多 Agent、Skill、上下文管理
- 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 准备
preparePermissionMatcher?(input): Promise<(pattern: string) => boolean>
}
每个字段都回答一个具体的工程问题:
- 没有
isConcurrencySafe——并发执行两个 Edit 会 race condition - 没有
interruptBehavior——工具运行中用户提交新消息时,无从区分"该中止的工具"和"该跑完的工具" - 没有
shouldDefer——所有工具 schema 全塞 prompt、token 爆掉 - 没有
backfillObservableInput——hook 看不到 legacy 字段 - 没有
checkPermissions——所有工具都能无条件执行、安全崩 - 没有
preparePermissionMatcher——hook 里Edit(src/**)这类规则式匹配无从按工具自己的输入语义(文件路径、Grep 的 pattern 等)去比对
一个字段 = 一类真实产品问题的沉淀。这是本专栏第一个 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 才知道。TypeScript 把"内部代码调错工具"这类问题提前到编译期;至于 LLM 生成的工具参数,它只在运行时才出现,任何语言都得在运行时校验——这正是 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 失败:grep 或命令输出几万行、塞爆上下文 ——Claude Code 的 maxResultSizeChars 超限即持久化到磁盘(Read 例外,它设为
Infinity、靠自身的 offset/limit 限量,见 §1.22) - Demo 失败:工具运行中用户插话,要么全停要么全等 ——Claude Code 的 interruptBehavior 让工具自己声明
- Demo 失败:每次 prompt 重传 40 工具 schema、token 爆 ——Claude Code 的 shouldDefer 延迟加载
- Demo 失败:LLM 写错工具名叫 old name ——Claude Code 的 aliases 兼容
- Demo 失败:第三方 MCP server 的工具被无条件执行 ——Claude Code 的 isMcp 标记 + 权限检查把它们与内置工具区别对待
- Demo 失败:hook 拿到的路径时而相对时而绝对、规则匹配失准 ——Claude Code 的 backfillObservableInput 在观察者看到前统一补齐
一百个小问题 × 真实生产的放大系数 = 51 万行。这就是为什么这个专栏要花几百页讲 Claude Code——每一万行都解决一类你在 demo 里想不到的问题。
读这个专栏的回报是什么?——下次你自己写 Agent 时、知道要考虑这些维度——不用等到生产环境被用户发现 bug 再回炉。
1.8 Harness 这个词的由来
本专栏反复用到 "Harness"(挽具)这个词——指 "让模型真的能用起来的所有工程代码"。Anthropic 自己也这么用:2025-09-29 宣布 Claude Code SDK 更名为 Claude Agent SDK 的文章,就把它称为 "the agent harness that powers Claude Code"。
词源自 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 的人多,能把 harness 做到生产级的人少。
读 Claude Code 源码、就是学世界顶级的 harness engineering。
1.9 和本系列其他专栏的关联
这个专栏是系列专栏中的一个——和其他几个有紧密关联:
- 《MCP 协议设计与实现》——Claude Code 内置完整的 MCP 客户端(
src/services/mcp/,约 25 个文件、40 万字符),本专栏第 11 章会反复引用《MCP 协议设计与实现》专栏的协议设计 - 《LangChain 设计与实现》——LangChain 的 Agent 抽象是框架化的另一条路,和 Claude Code 这种"单一产品内建 harness"的做法可以对照阅读
- 《LangGraph 设计与实现》——LangGraph 的 Pregel 模型和 Claude Code 的 Agent 循环(本专栏第 4 章)是两种不同的 agent runtime 范式,适合对照阅读
- 《React 19 内核探秘》——Claude Code 用 React + Ink 做终端 UI,本专栏第 17 章讲它自定义的 React reconciler,有 React 源码基础会读得更顺
- 《Serde 元编程》——Claude Code 大量用 Zod 做 schema validation(本专栏第 6 章),和 Serde 用 derive 从类型生成(反)序列化代码是同一类问题的两种解法,可以对照阅读
读这几个专栏合起来、才能完整把握 "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:进入查询循环(交互式 REPL 由 REPL.tsx 直接调用 src/query.ts 的 query();SDK 与 -p 才经过 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({ pattern: "FIXME|待办", glob: "*.{ts,tsx}" })。
Step 5:tool_use block 触发查询循环的 tool dispatch——找到 GrepTool,依次经过 validateInput → PreToolUse 钩子 → 权限检查(含工具的 checkPermissions)→ call → PostToolUse 钩子。
Step 6:GrepTool 的 call 方法——把参数拼成 ripgrep 命令行、以子进程运行,等它跑完后一次性拿到全部输出,再按 head_limit / offset 截取(2.1.89 的 cli.js 里 Grep 的 call 只解构前两个参数,不接 onProgress)。
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 到会话转录、以 JSONL 写到 ~/.claude/projects/<项目>/ 下供下次恢复(~/.claude/history.jsonl 另存的是你输入过的 prompt 历史,见第 16 章)。
这 10 步每一步都是 Claude Code 源码里的几百到几千行代码——整个 51 万行就是这 10 步的各种路径和边界情况。本专栏的 18 章就是把这 10 步拆开讲——每一步的设计动机、代码实现、工程权衡。
1.12 Claude Code 和传统 IDE 插件的边界
一个常见的比较——Claude Code vs GitHub Copilot / Cursor。先说清口径:下面每家的能力只取对方官方文档写明的(核对于 2026-09-24),不凭印象。三家今天都有 Agent 能力,差别主要在起点形态和主战场:
Copilot(从 IDE 内补全起步):
- 起点:2021 年 6 月技术预览,边打字边给补全建议
- 今天:GitHub 文档列出代码补全、Copilot Chat、agent mode("描述目标后多步执行:研究仓库、提出计划、编辑文件、运行工具"),以及在云端处理任务并开/更新 PR 的 coding agent
- 主战场:IDE 与 GitHub 平台本身
Cursor(VS Code 的 fork,编辑器即产品):
- 起点:编辑器内的对话与多文件编辑
- 今天:官方文档称 Cursor Agent 能"独立完成复杂编码任务、运行终端命令、编辑代码",自主搜索文件、跨多文件修改;另有 CLI 和网页端的 Agent 入口
- 主战场:编辑器
Claude Code(从终端 CLI 起步):
- 起点:2025 年 2 月以终端工具形态研究预览,显式命令(
claude)启动 - 今天:终端之外还有 IDE 扩展、桌面应用和网页入口(见 §1.1),但本专栏拆的是终端 CLI 那份代码
- 形态:对话 + 自主工具调用,2.1.89 的
cli.js里带searchHint的工具定义有三十多个(读写、搜索、命令、任务、MCP 资源、子 Agent 等)
终端 Agent 这条路也不止 Claude Code 一家:OpenAI 的 Codex CLI(官方仓库自述 "Lightweight coding agent that runs in your terminal",Apache-2.0)、Google 的 Gemini CLI("an open-source AI agent that brings the power of Gemini directly into your terminal",Apache-2.0)、开源的 Aider(在终端里与 LLM 结对编程)都属于这一类。
选哪个?——本专栏不做产品评测,这类比较变化极快,以各家当前文档为准。对本专栏更重要的是:无论哪家,一旦让模型"替你执行任务",就绕不开权限、并发、上下文、中断这一整套 harness 问题——而 Claude Code 是其中少有的、完整实现曾经可以被逐行阅读的一个。
1.13 为什么讲 TypeScript 而不是 Go/Rust 实现
有读者可能问——Anthropic 为什么用 TypeScript 写 Claude Code、不用更"性能的" Go 或 Rust?
几个原因:
(先声明:Anthropic 没有在官方文档里解释过这个选型,以下是本专栏的推测,不是官方说法。)
① 生态成熟度——LLM 生态的 SDK 以 Python 和 TypeScript 为主,Anthropic 官方也同时维护这两门语言的 SDK。
② 快速迭代——Claude Code 自 2025 年 2 月发布以来一直处于高速迭代期——TypeScript 的类型推导 + 热重载比 Rust 编译时间友好得多。
③ UI 生态——React + Ink 让终端 UI 能用前端的组件思维——Rust 的 tui-rs / Ratatui 要自己手写布局、开发效率明显更低。
④ 生态定位——面向广大开发者的工具,用 TypeScript 这样的最大公约数语言,写插件、MCP server、hook 脚本的门槛都更低(注意 Claude Code 本身并不开源)。
⑤ 性能不是瓶颈——Claude Code 的主要等待是LLM API 延迟、不是 JS 执行速度,两者差着数量级——换成 Rust 省下的那点执行时间,用户在等模型响应时几乎感知不到,优化 UI 状态比优化底层有意义。
⑥ 招聘池——全球 TypeScript 开发者的基数远大于 Rust——维护团队扩容容易。
TypeScript 的劣势——运行时类型擦除、需要 Zod 做 runtime validation、Node.js 内存占用高于 Rust 进程——但对这个产品形态,这些劣势可接受。
工具选择永远是 "用最适合场景的"——不是 trendy 的、也不是极限性能的、而是 "能快速交付优质产品的"。反过来也要看到,同类的 Codex CLI 就走了 Rust 路线(官方仓库里有 Rust 实现 codex-rs),两种选择各有代价。
1.14 Claude Code 源码目录结构全景
在进入后续章节前、扫一眼源码目录——让你对后续章节对应的文件位置有个地图。src/ 下面 53 个顶层目录/文件——按职责分组如下(只列代表性条目,不给各组行数:各组的精确行数本专栏没有统一实测,仅知 src/tools/ 为 42309 行,见 §1.17):
核心 runtime:
QueryEngine.ts(1295 行)——会话层编排,SDK 与-p的入口(主循环在query.ts)Tool.ts(792 行)——工具类型定义Task.ts(125 行)——后台任务管理commands/、commands.ts——slash commands 注册hooks/——各类 pre/post hook 的协议context/——对话上下文、消息流coordinator/——多 agent 协调
工具实现:
tools/(40+ 子目录)——每个工具独立文件夹services/mcp/——MCP 协议客户端实现services/lsp/——LSP 客户端(第 13 章)
UI 层:
components/——React + Ink 组件ink/、ink.tsx——Ink 集成dialogLaunchers.tsx——对话框触发器keybindings/——快捷键系统
入口和配置:
entrypoints/——不同入口(cli、sdk、bridge)cli/——CLI 解析和路由bootstrap/——启动前的预加载和初始化constants/——常量定义
持久化和迁移:
memdir/——会话记忆目录history.ts——对话历史migrations/——老版本数据迁移
bridge 和集成:
bridge/——和 IDE 的桥接协议native-ts/——原生 TypeScript 集成assistant/——assistant 配置和 profile
其他:
buddy/——终端里的"伙伴"(companion)形象及其即时反应(2.1.89 的cli.js里可见buddy_companion、/claude_code/buddy_react等字符串;不是一种 agent 模式)cost-tracker.ts、costHook.ts——成本追踪interactiveHelpers.tsx——交互辅助
这53 个顶层条目映射到本专栏的 18 章——每一章深入 1-3 个模块。你在读某一章时、可以直接翻到对应源码文件对照看——是本专栏的核心学习方式。
1.15 源码可见性的来源
这个专栏的契机是 2026 年 3 月底 Claude Code 完整源码通过 npm source map 流出:@anthropic-ai/claude-code@2.1.88(2026-03-30 发布,随后下架)的包里带了 .js.map 文件,它在 sourcesContent 字段里内嵌了原始 TypeScript 源码,用 source-map-explorer 一类工具就能还原回 .ts。次日发布的 2.1.89 已不再带 map。
至于这个 map 为什么会被打进发布包,Anthropic 没有公开过技术细节,本专栏不做揣测。
这种意外公开的边界很灰——map 文件确实随公开的 npm 包发出过,但 Anthropic 没有显式同意它被系统性研究。本专栏的处理方式是:只讲设计原则、不发布完整代码、引用最小必要片段,用于教学而非竞品复制。
1.16 术语约定
本专栏会用到一些特定术语——先统一约定、避免混淆:
- Agent——指 Claude Code 的某次运行实例(类似"进程")
- Sub-agent——指
AgentTool启动的子 Agent(可能与主 Agent 同进程,也可能是独立进程,见第 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 来个子目录里——光 FileEditTool 就有 1524 行(口径同下文),因为要处理:
- 文件不存在的 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、/schedule、/doctor、/hooks、/agents、/logout、/debug 等(这几个名字都能在 2.1.89 那份 npm 包的 cli.js 里按 name:"…" 的命令声明查到;/worktree、/sidebar 这两个在该快照里还不是斜杠命令——worktree 当时只有 EnterWorktree / ExitWorktree 两个工具,sidebar 则是 tengu_terminal_sidebar 这个默认关闭的终端状态位)。每个命令后面都有完整的 React 组件、slash 解析、参数校验——粗估平均每个 300 行、合计约 24k 行(这是量级估算,不是实测)。
数字三:40+ 键盘快捷键和 shortcuts——按键组合 + 上下文映射——输入框支持默认模式和 Vim 模式(2.1.89 的 editorMode 选项只有 normal 与 vim,旧值 emacs 会被当作 normal)——两种模式下同一键位可能有完全不同含义——键绑定系统本身就是一个小型状态机。
这三个数字撑起的是功能面的广度,不要拿它们去反推总行数——工具、命令、快捷键之间大量共享 utilities,简单相乘会重复计算。全仓规模以 §1.1 的口径说明为准。
这个量级的代码量在 CLI 工具里是极端罕见的——它更接近一个 IDE 级别的项目、而不是一个命令行程序;Claude Code 从 2025 年 2 月研究预览到这份 2026 年 3 月底的快照不过一年多、51 万行是惊人的工程产出密度。
1.18 第 2 章的骨架图预告
下一章会给出一张一屏图——把 Claude Code 整个系统画成这样:
┌──────────────────────────────────────────────────┐
│ CLI entry (claude) │
│ ↓ │
│ Bootstrap (parallel preload, feature flags) │
│ ↓ │
│ query() main loop (REPL / QueryEngine) │
│ ↓ ↑ │
│ ┌──────┴──────┐ │
│ │ LLM API │ ←→ Streaming (SSE) │
│ └──────┬──────┘ │
│ ↓ │
│ Tool Dispatch │
│ ↓ │
│ ┌──────────────────────┐ │
│ │ validateInput() │ │
│ │ PreToolUse hooks │ │
│ │ checkPermissions() │ ←→ Permission Engine │
│ │ call() + PostToolUse │ │
│ └──────────────────────┘ │
│ ↓ │
│ Tool Result │
│ ↓ │
│ React + Ink UI (streaming) │
│ ↓ │
│ History / Persistence │
└──────────────────────────────────────────────────┘
这张图的每一条线都对应一章——18 章读完、这张图就从简化的 box-and-arrow 变成带代码记忆的真实系统。
1.19 代码阅读方法论
分享一套读复杂 TypeScript 项目的方法论——在后续章节反复用到:
① 先读 type 定义、再读 implementation——Tool.ts 的 792 行里一半是类型定义——看懂这些类型就抓住了大部分设计意图。实现只是填充类型骨架的血肉。
② 入口优先——先看 main.tsx / cli/ 入口——顺着调用链下沉——比随机翻某个文件理解得快得多。
③ 找 central hub——QueryEngine / Tool / Task 这种被所有地方引用的核心类型——是理解整体的锚点。
④ 按"数据流"读、不是按"调用栈"读——跟踪用户输入怎么变成 LLM 响应、工具调用怎么回传结果——比读每个函数的调用关系顺得多。
⑤ 大量用 grep / search——找一个函数在哪被调——ripgrep + context lines——几秒知道一个函数的生命周期。
⑥ 画图同步——一边读一边在纸上/白板画 box-and-arrow——主动构造心智模型——比单纯看代码记得牢得多。
⑦ 对比类比——遇到新概念问自己 "像别的什么"——比如 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
}>
短短 20 来行代码、至少藏着 8 个深层工程决策:
① DeepImmutable<{...}> wrapper——让整个类型递归只读(包括 Map、包括嵌套对象)——防止中间层代码 mutate context。注意这只是编译期约束:类型擦除后运行时并不冻结对象,靠的是所有代码都过类型检查。
② 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 出来后能回到进入前的状态——避免用户丢失上下文。
20 来行代码 = 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+ 工具再加上 MCP 工具,如果 schema 全塞 prompt、会占掉可观的 context window。
解法是 ToolSearch 机制——把一部分工具标记为 deferred(2.1.89 的判定:alwaysLoad 为真的不延迟,MCP 工具一律延迟,内置工具看 shouldDefer)——模型一开始只看到这些工具的名字,完整定义不进模型上下文;要用时调 ToolSearch 搜索,命中后定义才被展开(请求层面的细节见《MCP 协议设计与实现》专栏第 19 章)。
searchHint 就是供 ToolSearch 做关键词匹配的——它只在本地打分时用,不发给模型。每个工具写一句 3–10 词的能力短语,让模型搜 "jupyter" 时 ToolSearch 能匹配到 NotebookEdit(其 searchHint 为 "edit Jupyter notebook cells (.ipynb)")。
三个细节约束:
- 3–10 words——太短关键词不够、太长会稀释匹配
- "no trailing period"——避免拼接时出现 "jupyter. use for..." 的标点问题
- "Prefer terms not already in the tool name"——别重复工具名本身、扩展搜索命中率
这个策略的量级(纯假设性粗算,不是实测值):
- 无 ToolSearch:假设 40 个工具、每个定义约 500 token = 约 20,000 token
- 有 ToolSearch:假设 5 个常驻工具 × 500 + 35 个 deferred 工具只列名字(每个按 10 token 算)= 约 2,850 token
- 在这组假设下,工具相关 token 省掉八成以上
省下的 token 给对话历史 + 代码上下文,让 Claude Code 能在同样 context window 下处理更长的任务。
这个小字段背后是整个 token 经济学——40+ 工具、3–10 词的 searchHint 规范、"只给名字 → 搜索 → 展开定义"的延迟加载,合起来是 Claude Code 能使用这么多工具而不炸 context window 的关键。本专栏第 6 章会回到 searchHint 与工具 Schema 的延迟加载。
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 时、写到磁盘、只回给 LLM 一个预览 + 文件路径。2.1.89 的 cli.js 里,多数工具设的是 100,000 字符,Bash / PowerShell 为 30,000,Grep 为 20,000,Read 为 Infinity。LLM 想看完整输出、用 Read 工具读那个文件——按需加载。
Infinity 的特殊值——Read 工具的输出不能持久化——因为 "持久化 Read 的结果到文件、再 Read 那个文件" 会形成无限循环。Read 靠自己的 offset/limit 机制限量。
这种"结果太大自动持久化、LLM 按需加载" 的模式是 LLM agent 必备机制——否则一次 grep 能把整个 context 吃光。
实现细节——Claude Code 内部有 ContentReplacementState 机制(utils/toolResultStorage)——把持久化的内容替换成 placeholder、后续 LLM 可以 reference。
这个机制的巧妙处——对 LLM 透明——LLM 看到的 "结果太大、已存到会话目录下 tool-results/ 里的某个文件" 就当作正常的工具输出、自然地继续 chain-of-thought——Read 一下就拿到详情。
一个 number field + 几行持久化逻辑 = LLM agent 能处理任意大小输出的关键 trick。
1.23 interruptBehavior ——工具运行中来了新消息怎么办
最后一个字段——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。执行器取不到该方法(或调用抛错)时一律按 block 处理;只有当前有声明为 cancel 的工具在跑,提交新消息才会以 "interrupt" 为理由中止本轮(遥测事件 tengu_cancel,source 为 interrupt_on_submit;优先级为 now 的排队消息也会触发同样的中止)。
实际情况:在 2.1.89 的公开包里,interruptBehavior 只出现在执行器读取它的地方,没有任何内置工具覆盖它——也就是说 Bash、Edit、Grep、Agent 等全部走默认的 block。这个接口是给"可以安全丢弃结果"的工具预留的(是否有内部构建里的工具用到,公开包看不出来)。
这种 "允许工具自定义中断语义" 的设计比 "统一 cancel / 统一 block" 更合理——因为不同工具的重启代价不同。
别和 Esc / Ctrl-C 混为一谈——interruptBehavior 只管"工具运行中用户又提交了一条消息"这一种情况。用户主动按 Esc 或 Ctrl-C 取消,走的是另一条路:直接触发 abort,把当前轮次停下来(见第 5 章);连按两次 Ctrl-C 则是退出程序(界面提示 "Press Ctrl-C again to exit")。
这个字段的存在提醒我们——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 看到完整信息?
场景(示意,非源码):假设某工具的输入字段从 path 改名为 file_path,而外部的 hook 脚本还在读老字段。模型按新 schema 调用,写 hook 的人却不一定跟着改。
解法——backfillObservableInput 在 input 传给 observers 前补上 legacy / 派生字段:
// 示意:为 observer 副本补回老字段名
backfillObservableInput(input) {
if (typeof input.file_path === 'string' && input.path === undefined) {
input.path = input.file_path
}
}
observer 们(hook、transcript、SDK 流)看到的 input 既包含新字段也包含老字段——老代码依然能读到期望的字段、新代码也能读新字段。2.1.89 里真实的用法更朴素:Edit、Write 等文件工具用它把 file_path 展开成绝对路径,让 hook 与权限规则看到的路径始终一致(见第 8 章 §8.6.2)。
注意注释里的三个约束:
- "Mutate in place"——直接改 input 对象、不返回新 object——省内存
- "Must be idempotent"——可能被多次调用、第二次调用不该产生不同结果
- "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 产品的常见优化——缓存命中能显著压低重复前缀部分的输入 token 成本。
这个字段的存在说明,在这份代码里,可能影响缓存的路径是被专门考虑过的。下一章进入架构全景图。