Claude Code 源码深度解析

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

作者 杨艺韬 · 8,049 字

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

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 协议集成:    ~120,000 行(客户端 + OAuth + 服务发现)
权限规则引擎:    ~30,000 行(5 种模式 + 动态分类器)
终端 UI:         ~50,000 行(React + Ink 组件库)

作为对比:一个典型的"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 你将学到什么

读完本书,你将理解:

系统架构层面

工程实践层面

设计思维层面

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

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
}

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

30 个字段 = 30 类真实产品问题的沉淀。这是本书第一个 takeaway——简单的"工具"概念在生产级 Agent 系统里变成 30 维空间。学会识别这 30 个维度、你就知道评估任何 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 里的一个失败模式:

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

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

1.8 Harness 这个词的由来

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

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

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

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

1.9 和本系列其他书的关联

这本书是 15 本系列的一本——和其他几本有紧密关联:

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

1.10 阅读建议

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

如果你是前端工程师——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 内嵌):

Cursor(IDE Fork):

Claude Code(终端 CLI):

三者服务不同的心智距离——Copilot 是 "加速你打字"、Cursor 是 "协助你编辑"、Claude Code 是 "替你执行任务"。距离越远、需要的 harness 越多——这就是 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 源码目录结构全景

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

核心 runtime(~100k 行)

工具实现(~150k 行)

UI 层(~50k 行)

入口和配置(~30k 行)

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

bridge 和集成(~40k 行)

其他(~120k 行)

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?推测原因:

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

1.16 术语约定

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

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

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

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

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

FileEdit 这一个工具的复杂度、就超过你想象的"用 fs.writeFile 一行搞定"几十倍。每个工具都如此——40 × 2000 = 80 万行工具代码(实际统计约 60 万行、算上 shared utilities)。

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

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

这三个数字合计约 70 万行——还没算 MCP、权限引擎、UI、bridge、bootstrap 等——51 万行只是核心 src/、所有 build artifacts 加起来应该是 80-100 万行级

这个量级的代码量在 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:123-148ToolPermissionContext

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。

两个细节约束

这个策略的数字效果

省下的 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"——怎么办?

两种策略

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

典型选择

这种 "允许工具自定义中断语义" 的设计比 "统一 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 成本的执着:每一个可能影响缓存的代码路径都经过精心设计。下一章进入架构全景图。