0%

探索 Claude code 技术架构

对 Anthropic 开源仓库 claude-code 的技术架构进行源码级深度解析,涵盖代理循环、上下文管理与 Memory 系统、Skill 触发机制、Hook 事件系统、多代理编排与 Worktree 隔离、MCP 集成架构、沙箱安全模型。

Intro

最近工作有点忙,做了一点Agent相关的工作,好久没写文章了,今天弥补一下。Claude Code 是 Anthropic 推出的代理型编码工具(Agentic Coding Tool),运行在终端中,通过自然语言驱动代码理解、生成、审查和 Git 工作流自动化。

该仓库(github.com/anthropics/claude-code并非核心运行时源码——CLI 引擎以编译后的 npm 包 @anthropic-ai/claude-code 分发,核心代码闭源。仓库中开放的是:插件系统、Hook 实现、配置示例、GitHub Actions 工作流和 DevContainer 沙箱配置。但结合 CHANGELOG(2600+ 行)和插件源码,已经足够还原出完整的技术架构。

层面 技术
运行时 Node.js 18+,TypeScript,React Compiler(TUI 渲染)
分发 平台原生安装器 / Homebrew / WinGet / npm(已废弃)
终端 UI 自研 TypeScript 渲染引擎,Yoga WASM 布局(v2.1.69 后替换为纯 TS)
插件脚本 Python 3、Bash、Node.js
容器化 Docker / Podman + DevContainer
沙箱 macOS Seatbelt + Linux iptables/ipset
协议 MCP (Model Context Protocol)、OAuth 2.0 (RFC 9728)
遥测 OpenTelemetry (OTEL),支持 traces/metrics/logs 三通道

整体架构

Claude Code 的整体架构分为 7 层,自上而下分别是:

  1. 用户交互层:支持 4 种入口——终端 CLI(核心)、VSCode 扩展(嵌入式)、GitHub @claude(CI/CD 集成)、TypeScript/Python SDK(编程式调用)。所有入口最终都汇入同一个代理循环。

  2. 代理循环:核心引擎,实现了工具增强的 ReAct 循环。每一轮包含:命令解析 + Skill 语义匹配 → 上下文构建(注入 Memory、Skill、系统提示)→ LLM 推理 → 工具调用 → 结果反馈 → 回到 LLM。循环持续直到模型决定停止或用户中断。

  3. 上下文管理:最复杂的子系统。负责在有限的上下文窗口(最大 1M tokens)内精确管理信息密度。包含 MEMORY.md(手动记忆)、Auto-Memory(自动记忆提取)、Auto-Compaction(对话压缩)、Skill Loading(按需知识注入)和磁盘持久化(大结果卸载)五个子模块。

  4. 工具层:代理可调用的所有工具。分为文件操作(Read/Write/Edit)、Shell 执行(Bash/PowerShell)、搜索(Glob/Grep/WebSearch)、代理编排(Agent/SubAgent/Task)、MCP 外部工具和辅助工具(TodoWrite/CronCreate)六类。

  5. 扩展层:插件系统提供的四种扩展原语——Plugin(容器)、Hook(事件拦截)、Skill(知识注入)、Agent(专业代理定义)。

  6. 安全层:五层纵深防御——权限规则、文件沙箱、网络防火墙、凭证保护、企业策略。每一次工具调用都要经过安全层的检查。

  7. API 层:底层 LLM 调用。支持 Anthropic 直连、AWS Bedrock、Google Vertex AI、Microsoft Foundry 四种后端,通过环境变量或配置切换。


代理循环(Agent Loop)

Claude Code 的核心是一个工具增强的 ReAct 代理循环。从 CHANGELOG 和插件系统的设计可以还原出完整的运行流程:

与简单的 ReAct 循环相比,Claude Code 做了四个关键增强:

  1. 15+ 种 Hook 拦截点:覆盖了代理循环的每一个关键节点。Hook 不仅能拦截,还能修改——UserPromptSubmit Hook 可以通过 updatedInput 重写用户输入,PreToolUse Hook 可以修改工具参数,Stop Hook 可以通过 block 强制代理继续工作。

  2. 自动上下文压缩:当对话接近上下文窗口限制时,自动触发 Compaction。压缩前后分别有 PreCompactPostCompact Hook,允许外部系统保存和恢复关键状态。

  3. 大结果磁盘卸载:工具返回超过 50K 字符的结果时,自动持久化到磁盘,上下文中只保留文件引用。这显著延长了对话寿命。

  4. 多代理编排:通过 Agent/SubAgent/TaskCreate 工具,支持并行派生子代理。每个子代理运行独立的代理循环,拥有独立的上下文窗口。

内置工具体系

从 CHANGELOG 和配置文件中可以提取出完整的工具列表:

工具 功能 权限 备注
Bash Shell 命令执行 需审批/沙箱 支持 Bash(git:*) 细粒度过滤,支持通配符 Bash(npm *)
PowerShell Windows PS 执行 需审批 v2.1.84 起 opt-in 预览
Read 读取文件 默认允许 v2.1.87 起使用紧凑行号格式,去重未变更的重复读取;PDF 支持 pages: "1-5" 参数
Write 写入/创建文件 需审批 保留 CRLF 行尾,尊重系统 umask
Edit / MultiEdit 编辑文件 需审批 并行编辑互不阻塞,单个失败不影响兄弟;sed 命令自动渲染为 diff 预览
WebSearch / WebFetch 网络搜索/抓取 可配置 WebFetch 标识为 Claude-User,二进制内容自动保存到磁盘
Glob / Grep / LS 文件搜索 默认允许 排除 .git/.jj/.sl 等 VCS 目录
Agent / SubAgent 创建子代理 内部 支持 model/isolation/name 参数
SendMessage 向已有代理发消息 内部 自动恢复已停止的代理
TaskCreate 创建后台任务 内部 输出超过 5GB 自动 kill;输出超过 30K 字符自动截断并附文件引用
WorktreeCreate 创建 Git Worktree 需审批 支持 worktree.sparsePaths 稀疏检出
ToolSearch 动态工具发现 内部 延迟加载工具,减少初始上下文占用
SlashCommand 调用斜杠命令 内部 可通过 disable-model-invocation 禁止 LLM 自动调用
TodoWrite 任务列表管理 内部 跟踪多阶段工作流进度
AskUserQuestion 向用户提问 内部 PreToolUse Hook 可通过 updatedInput 自动回答
CronCreate 定时任务 内部 v2.1.71 起支持 /loop 定时执行

ToolSearch:延迟加载与动态发现

ToolSearch 是一个值得深入分析的设计。传统做法是在系统提示中列出所有可用工具的 schema,但当用户配置了大量 MCP 工具时,工具描述本身就会占据大量上下文。

Claude Code 的解决方案是工具的延迟加载

关键技术细节:

  • 自动阈值:v2.1.7 引入 auto:N 语法,N 是上下文窗口百分比(0-100),默认 10%。当 MCP 工具描述超过此阈值时自动启用延迟加载。
  • Schema 注入方式:v2.1.70 修复了 ToolSearch 返回的工具 schema 以 system-prompt-style tags 追加到 prompt 尾部,这可能导致模型过早停止。
  • Compaction 兼容:v2.1.76 修复了通过 ToolSearch 延迟加载的工具在 Compaction 后丢失 input schema 的问题,导致数组和数字参数被类型错误拒绝。
  • Cache 兼容:v2.1.84 实现了全局系统提示缓存在 ToolSearch 启用时也能工作。

上下文管理与 Memory 系统

这是 Claude Code 最核心也最复杂的子系统。LLM 的上下文窗口是有限的(Opus 4.6 支持 1M tokens,可通过 CLAUDE_CODE_DISABLE_1M_CONTEXT 禁用),长会话必然面临上下文溢出问题。Claude Code 通过五个机制来管理上下文:Auto-CompactionPartial SummarizationMEMORY.mdAuto-Memory磁盘持久化

Auto-Compaction(自动上下文压缩)

当对话长度接近上下文窗口限制时,Claude Code 会自动触发 Compaction。完整流程:

Compaction 的关键技术细节(从 CHANGELOG 修复记录中提取):

1. Token 估算精度

Compaction 的触发依赖于准确的 token 估算。v2.1.75 修复了 thinking blocks 和 tool_use blocks 的 token 估算过高问题——之前的估算算法会导致过早触发 Compaction,浪费上下文空间。v2.1.14 修复了上下文窗口阻塞限制计算过于激进的问题——之前在 ~65% 使用率时就阻塞用户,实际应该在 ~98% 时才阻塞。v2.1.7 还修复了阻塞限制使用完整上下文窗口而非有效上下文窗口(需要为 max output tokens 预留空间)的问题。v2.1.9 修复了 auto-compact 在大输出 token 限制的模型上触发过早的问题。

2. 压缩请求的大小管理

v2.1.85 修复了当对话过大时,压缩请求本身超出上下文限制的问题。这意味着 Compaction 不是简单地把全部历史发给 LLM,而是有一个分段处理机制。具体策略:

  • PDF 和 document blocks:在压缩前被剥离(v2.1.47 修复了包含大量 PDF 时 Compaction 失败的问题)
  • 图片:保留在压缩请求中(v2.1.69),这样可以复用 prompt cache,使压缩更快更便宜
  • 子代理 progress payloads:被剥离(v2.1.63),这些 heavy payloads 在压缩后不再需要
  • CLAUDE_CODE_MAX_OUTPUT_TOKENS:在 Compaction 期间也被尊重(v2.1.69 修复了之前被忽略的问题)

3. 状态保持

Compaction 后需要保持多种状态,这是最容易出 bug 的地方。Plan mode(规划模式)需要在压缩后保持,否则模型会从规划模式意外切换到实现模式。重命名的会话标题需要保持。后台子代理的可见性需要保持,否则主代理会重复创建已经存在的代理。子代理调用的 Skill 不能在压缩后泄漏到主会话的上下文中。通过 ToolSearch 延迟加载的工具需要在压缩后保留完整的 input schema,否则数组和数字参数会被类型错误拒绝。后台代理完成通知中的输出文件路径也需要保持。

4. 熔断机制

v2.1.76 引入了 Auto-Compaction 的熔断器——连续失败 3 次后停止重试,避免无限循环。v2.1.59 修复了 auto-compact 失败错误通知被显示给用户的问题。

5. 恢复行为

v2.1.69 改变了 Compaction 后恢复对话的行为——不再产生 preamble recap(“让我回顾一下之前的对话…”),而是直接继续。这减少了不必要的 token 消耗。

6. 内存清理

Compaction 不仅压缩上下文,还触发内存清理——长会话中的 progress messages 在 Compaction 后需要被清除,否则会导致内存持续增长;内部缓存需要在 Compaction 后重置;in-process teammates 的完整对话历史需要在 Compaction 后解除引用,否则会阻止垃圾回收。

用户也可以通过 /compact 命令手动触发压缩,或通过 /context 命令查看当前上下文使用情况——v2.1.74 增加了 /context 的 actionable suggestions,能识别上下文占用大户(如 MCP 工具描述、memory 膨胀等)并给出优化建议。

Partial Summarization(部分对话摘要)

v2.1.32 引入了 “Summarize from here” 功能——用户可以在消息选择器中选择一个起点,只对该点之后的对话进行摘要,而保留之前的完整历史。

这与 Auto-Compaction(压缩全部历史)不同,Partial Summarization 允许用户精确控制哪些对话被压缩、哪些被保留。典型场景:一个长会话中前半部分是重要的架构讨论(需要保留原文),后半部分是琐碎的调试过程(可以压缩为摘要)。

v2.1.30 的修复记录显示,conversation summarization 也会遇到 token limit 错误,说明摘要生成本身也使用 LLM 调用,受上下文窗口限制。

MEMORY.md(手动 Memory)

用户通过 /memory 命令管理的持久化记忆文件,存储在 ~/.claude/MEMORY.md。这个文件在每次会话开始时被注入到系统提示中,作为 Claude 的"长期记忆"。

MEMORY.md 有严格的大小限制——最大 25KB 或 200 行,超出部分会被截断。这是为了防止 Memory 文件无限膨胀导致上下文被挤占。系统还会为 Memory 文件添加 last-modified 时间戳,帮助 Claude 判断哪些记忆是新鲜的、哪些是过时的。/context 命令会将 Skills 作为独立类别显示,帮助用户理解上下文的组成。

Auto-Memory(自动 Memory 提取)

v2.1.32 引入的核心特性——“Claude automatically records and recalls memories as it works”。Claude 在对话过程中自动提取有用的上下文并保存到 auto-memory

Auto-Memory 的存储位置默认在 ~/.claude/ 目录下,可通过 autoMemoryDirectory 设置自定义目录。一个重要的设计决策是 auto-memory 在同一仓库的不同 git worktree 之间共享——这意味着在 worktree A 中学到的知识,在 worktree B 中也能使用。--bare 模式会完全禁用 auto-memory、hooks、LSP、plugin sync 和 skill directory walks。

Auto-Memory 作为异步后台任务运行,这带来了一个并发问题:它的写入可能与主会话的 transcript 写入冲突,导致 --resume 时丢失最近的对话历史。Session 恢复的内存使用也经过了优化——通过将 session index 替换为基于 stat 的轻量级加载和渐进式丰富,--resume 的内存使用降低了 68%。

工具结果的磁盘持久化

除了 Memory 系统,Claude Code 还有一个重要的上下文管理机制:大工具结果的磁盘持久化

1
2
3
4
5
工具返回结果 → 检查大小 → 超过 50K 字符?
├─ 否 → 直接注入上下文
└─ 是 → 写入磁盘文件 → 上下文中保留文件路径引用

cleanupPeriodDays 后自动清理

v2.1.51 将阈值从 100K 字符降低到 50K 字符。v2.1.50 修复了 cleanupPeriodDays 设置被忽略导致临时文件永远不会被清理的问题。v2.1.84 修复了长会话中大工具结果在处理后未被清理的内存泄漏。

后台任务(TaskCreate)的输出也有类似机制:v2.1.59 修复了 API context overflow——当后台任务产生大量输出时,自动截断到 30K 字符并附上文件路径引用。

空闲会话处理

v2.1.84 增加了一个巧妙的设计:当用户离开超过 75 分钟后返回时,会弹出一个提示建议执行 /clear。这是因为长时间空闲后,之前的上下文可能已经过时,继续在旧上下文上对话会导致不必要的 token re-caching 开销。


Skill 触发与加载机制

Skill 系统是 Claude Code 实现领域知识注入的核心机制。与 Hook(事件驱动)和 Command(用户显式调用)不同,Skill 是语义驱动的自动触发

三级渐进式加载

这种设计的核心思想是:不要把所有信息都塞进上下文,而是让 LLM 自己决定什么时候需要什么信息

v2.1.32 引入了 Skill 字符预算的动态缩放——预算为上下文窗口的 2%,这样拥有更大上下文窗口的用户可以看到更多 Skill 描述而不会被截断。v2.1.87 将 /skills 列表中的 description 截断到 250 字符

触发机制详解

Skill 的触发依赖于 SKILL.md frontmatter 中的 description 字段。这个字段不是给人看的——它是给 Claude Code 的 Skill 匹配引擎看的。官方文档要求使用第三人称 + 具体触发短语的格式:

1
2
3
4
5
6
7
8
# 好的 description(具体的触发短语)
description: >
This skill should be used when the user asks to "create a hook",
"add a PreToolUse hook", "validate tool use", "implement prompt-based hooks",
or mentions hook events (PreToolUse, PostToolUse, Stop).

# 差的 description(模糊、没有触发短语)
description: Provides hook guidance.

当用户输入与某个 Skill 的 description 语义匹配时,Claude Code 会将该 Skill 的 SKILL.md 正文加载到上下文中。SKILL.md 正文中会引用 references/examples/scripts/ 目录下的资源,Claude 可以根据需要主动读取这些资源。

v2.1.59 修复了 Claude 有时会冗余地调用 Skill 工具(即使直接运行斜杠命令时)的问题。v2.1.59 还改进了 Skill 建议的优先级——优先推荐最近和最常使用的 Skill。

Skill 的条件触发

v2.1.84 引入了 paths frontmatter 字段,支持基于文件路径的条件触发:

1
2
3
4
5
6
7
8
---
name: frontend-design
description: This skill should be used when working on frontend components
paths:
- "src/components/**"
- "src/pages/**"
- "*.tsx"
---

只有当用户正在操作匹配 glob 模式的文件时,该 Skill 才会被激活。这进一步减少了不相关 Skill 对上下文的污染。

v2.1.69 还引入了 ${CLAUDE_SKILL_DIR} 变量,允许 Skill 在 SKILL.md 中引用自身目录下的文件,实现了 Skill 的自包含性。

Skill 的资源组织

Skill 的资源分为四类,每类有不同的加载语义:

目录 用途 加载方式 上下文影响
references/ 参考文档(API 文档、Schema、策略) Claude 主动 Read,按需加载 读取时占用上下文
examples/ 可运行的代码示例 Claude 主动 Read,可直接复制 读取时占用上下文
scripts/ 可执行脚本(Python/Bash) Claude 通过 Bash 工具执行 无需读入上下文
assets/ 输出资源(模板、图片、字体) Claude 通过工具操作 无需读入上下文

scripts/assets/ 的关键优势是:它们可以被使用而不需要被读入上下文窗口。一个 Python 脚本可以直接通过 Bash 工具执行,Claude 只需要知道脚本的路径和参数格式,不需要把整个脚本内容加载到上下文中。

自动发现机制

Claude Code 在启动时自动扫描以下位置的 Skill:

1
2
3
4
5
1. 项目级:.claude/skills/*/SKILL.md
2. 插件级:plugins/*/skills/*/SKILL.md
3. 用户级:~/.claude/skills/*/SKILL.md
4. 附加目录:--add-dir 指定的目录中的 .claude/skills/*/SKILL.md
5. 嵌套发现:子目录中的 .claude/skills/ (v2.1.6+)

Skill 的自动发现需要处理几个安全和性能问题。嵌套发现可能会意外加载 gitignored 目录(如 node_modules)中的 Skill,需要过滤。--setting-sources user 参数需要正确阻止动态发现的项目 Skill。为了提升启动性能,commands、skills 和 agents 会从磁盘缓存加载,无需每次重新扫描文件系统。此外,Skill description 中包含冒号(如 “Triggers include: X, Y, Z”)会导致 YAML frontmatter 解析失败,需要特殊处理。


插件系统架构

插件系统是 Claude Code 最核心的扩展机制,采用声明式 + 约定式设计。

插件目录结构与发现

1
2
3
4
5
6
7
8
9
10
11
plugin-name/
├── .claude-plugin/
│ └── plugin.json ← 插件清单(唯一必需文件)
├── commands/ ← 斜杠命令(Markdown + YAML Frontmatter)
├── agents/ ← 专业代理定义
├── skills/ ← 技能(语义触发的上下文增强)
├── hooks/ ← 事件钩子
│ └── hooks.json
├── settings.json ← 插件默认配置(v2.1.49+)
├── .mcp.json ← MCP 服务器配置
└── README.md

Claude Code 在启动时的组件发现流程:

plugin.json 最小配置只需一个 name 字段(kebab-case,正则 /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/)。路径必须以 ./ 开头,不允许 ../ 或绝对路径。

Markdown as Code:Frontmatter 协议

命令和代理都是 Markdown 文件,通过 YAML Frontmatter 声明元数据。这是 Claude Code 最巧妙的设计——用 Markdown 同时承载了"给 LLM 的 Prompt"和"给系统的配置"。

Frontmatter 支持的完整字段:

字段 类型 作用
allowed-tools String/Array 限制可用工具,支持 Bash(git:*) 细粒度过滤,支持 YAML 列表语法
disallowedTools Array 禁止使用的工具列表(v2.1.78+)
model String 指定模型:haiku / sonnet / opus / inherit
effort String 模型努力级别:low / medium / high
maxTurns Number 最大对话轮次
argument-hint String 参数提示,如 [pr-number]
disable-model-invocation Boolean 禁止 LLM 自动调用此命令
user-invocable Boolean 是否在斜杠命令菜单中显示(默认 true)
initialPrompt String 自动提交的首轮提示(v2.1.83+)
paths Array 条件触发路径(YAML glob 列表,v2.1.84+)
background Boolean 始终作为后台任务运行(v2.1.49+)
isolation String worktree = 在隔离的 git worktree 中运行(v2.1.49+)
hooks Object 代理级 Hook 定义(PreToolUse/PostToolUse/Stop,v2.1.59+)

allowed-tools 的过滤语法支持多种模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 精确命令匹配
allowed-tools: Bash(git:*), Read, Write

# 通配符匹配(v2.1.59+)
allowed-tools: Bash(npm *), Bash(* install), Bash(git * main)

# MCP 工具匹配
allowed-tools: mcp__github_inline_comment__create_inline_comment

# YAML 列表语法(v2.1.59+)
allowed-tools:
- Bash(git:*)
- Read
- Write

插件来源与分发

来源 说明 更新策略
git Git 仓库(支持 @ref 指定分支/标签) ref-tracked 自动更新(v2.1.81)
git-subdir Git 仓库的子目录(v2.1.69+) 同 git
npm npm 包(支持自定义 registry 和版本锁定) 手动更新
local 本地目录 实时
settings settings.json 内联声明(v2.1.80+) 随配置变更

v2.1.81 引入了 ref-tracked 插件的自动更新——每次加载时重新 clone,确保始终使用上游最新版本。v2.1.14 支持将插件 pin 到特定 git commit SHA。

插件选项系统

v2.1.83 引入了 manifest.userConfig,允许插件声明用户可配置的选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "my-plugin",
"userConfig": {
"apiKey": {
"type": "string",
"description": "API key for the service",
"sensitive": true
},
"maxRetries": {
"type": "number",
"default": 3
}
}
}

sensitive: true 的值存储在 macOS Keychain 或其他平台的受保护凭证文件中,而非明文配置。


Hook 事件系统

Hook 系统是 Claude Code 实现行为可编程性的核心机制。它在代理循环的关键节点插入了可编程的拦截点。

完整事件生命周期

Hook 事件的生命周期严格遵循代理循环的执行顺序。一次完整的会话从 SessionStart 开始,系统首先加载 CLAUDE.md 指令文件并触发 InstructionsLoaded,随后进入代理循环的主体。每一轮用户交互都以 UserPromptSubmit 开始——这是用户输入进入系统的第一个拦截点。如果当前上下文已经接近窗口上限,系统会在 LLM 推理之前触发 PreCompact → 压缩 → PostCompact 序列。LLM 推理完成后,如果模型决定调用工具,则进入 PreToolUse → 执行 → PostToolUse 的工具执行管线;工具执行过程中可能触发一系列副作用事件——文件变更触发 FileChanged,创建子代理触发 TaskCreated,创建 Worktree 触发 WorktreeCreate,MCP 服务器请求用户输入触发 Elicitation。如果 LLM 决定停止,则触发 Stop(主代理)或 SubagentStop(子代理),最终以 SessionEnd 结束会话。如果 API 调用本身出错(网络超时、速率限制等),则触发 StopFailure 而非正常的 Stop

值得注意的是,ConfigChangeNotification 是两个不在主循环中的旁路事件——ConfigChange 在用户修改 settings.json 或 CLAUDE.md 时触发,Notification 在系统发送桌面通知时触发。它们可以在会话的任何时刻发生,不受代理循环阶段的约束。

事件分类与控制能力

从控制能力的角度,这 20 种事件可以分为三类:

可决策事件——Hook 的返回值直接影响代理的下一步行为。PreToolUse 是最强大的可决策事件:Hook 可以返回 allow 直接放行、deny 拒绝执行并注入 systemMessage 告知模型原因、ask 交由用户审批,还可以通过 updatedInput 修改工具的输入参数(例如将危险的 rm -rf / 改写为 rm -rf ./build/),或通过 additionalContext 向模型注入额外的上下文信息。StopSubagentStop 同样是可决策事件——Hook 可以返回 block 强制代理继续工作(配合 systemMessage 告诉模型为什么不能停),这在自动化流水线中非常有用,可以确保代理完成所有预定步骤才停止。UserPromptSubmit 可以通过 updatedInput 重写用户的原始输入——典型用途是自动补充上下文、翻译语言或注入安全约束。

可观测事件——Hook 可以观察到事件发生并注入信息,但不能改变代理的决策方向。PostToolUse 属于此类:Hook 可以在工具执行完成后通过 systemMessage 向模型注入反馈(例如“这个文件已经被修改,请重新读取”),但不能阻止工具执行或修改其结果。PostCompact 也是可观测事件——它允许 Hook 在上下文压缩完成后通过 systemMessage 重新注入关键信息(例如压缩前的重要决策、未完成的任务列表),确保压缩不会丢失关键状态。

纯通知事件——Hook 只能观察,不能注入任何信息。SessionStartSessionEndFileChangedCwdChangedTaskCreatedConfigChangeNotification 等都属于此类。它们的主要用途是审计日志、外部系统同步和环境初始化。其中 SessionStart 是一个特例——它虽然不能通过返回值影响代理行为,但可以通过 $CLAUDE_ENV_FILE 机制持久化环境变量,间接影响整个会话的行为。

三种 Hook 类型与通信协议

Claude Code 提供了三种 Hook 实现方式,分别面向不同的使用场景和技术栈。

Command Hook 是最基础也最灵活的类型——它通过 spawn 启动一个外部子进程,将事件上下文以 JSON 格式写入子进程的 stdin,然后从 stdout 读取 JSON 格式的返回值。子进程的退出码决定了默认行为:退出码 0 表示允许,退出码 2 表示阻止。这意味着即使脚本没有输出任何 JSON,系统也能通过退出码做出决策。所有 Command Hook 都会收到一组共享的上下文字段——session_id(会话标识)、transcript_path(完整对话记录的文件路径,脚本可以读取这个文件来分析对话历史)、cwd(当前工作目录)、permission_mode(当前权限模式)、hook_event_name(事件名称)。如果当前运行在子代理或 Worktree 中,还会附加 agent_idagent_typeworktree(包含 name、path、branch、originalRepoDir 四个字段)信息。Command Hook 可以用任何语言编写——Python、Bash、Node.js、Go 编译的二进制文件都可以,只要它能从 stdin 读取 JSON 并向 stdout 写入 JSON。

Prompt Hook 是一种更高层次的抽象——它不执行外部脚本,而是将事件上下文嵌入到一个 prompt 模板中,发送给 LLM 进行智能判断。系统支持 $TOOL_INPUT$TOOL_RESULT$USER_PROMPT$TRANSCRIPT_PATH 等变量插值,这些占位符会在发送前被替换为实际值。LLM 的返回值会被解析为 allow/deny/block 决策。Prompt Hook 的优势在于它能处理模糊的、需要语义理解的判断——例如"这个 Shell 命令是否可能删除重要数据"或"代理的回答是否完整解决了用户的问题"。但它的限制也很明显:每次触发都会产生一次 LLM API 调用,带来额外的延迟和成本;且仅支持 StopSubagentStopUserPromptSubmitPreToolUse 四种事件。

HTTP Hook 是面向分布式系统的类型——它将事件上下文以 JSON 格式 POST 到一个远程 HTTP 端点,字段结构与 Command Hook 的 stdin 完全相同。远程服务返回 HTTP 200 + JSON 响应体,响应格式也与 Command Hook 的 stdout 相同。HTTP Hook 的优势在于它可以将审计、合规检查、权限管控等逻辑集中到一个中央服务,而不是分散在每个开发者的本地脚本中——这对企业级部署尤其重要。

条件触发与并行执行

Hook 的触发支持两级过滤。第一级是 matcher 字段——它指定了 Hook 关注的工具名称(仅对 PreToolUsePostToolUse 有效)。第二级是 if 条件字段——它使用与权限规则相同的语法进行更细粒度的过滤。例如,一个 Hook 的 matcher 设为 Bash(匹配所有 Bash 调用),但 if 设为 Bash(git *)(只匹配 git 命令),这样就能精确拦截 git 操作而不影响其他 Shell 命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash validate-git.sh",
"if": "Bash(git *)"
}
]
}
]
}

此外,once: true 配置可以让 Hook 只在首次匹配时触发一次——典型用途是 SessionStart 时的一次性环境检测,或者首次使用某个工具时的初始化逻辑。

在执行模型上,Claude Code 做了一个重要的设计决策:同一事件的所有匹配 Hook 并行执行。当一个 PreToolUse 事件同时匹配了三个 Hook 时,这三个 Hook 会被同时启动(Command Hook 同时 spawn 三个子进程,HTTP Hook 同时发出三个请求),系统等待所有 Hook 完成后合并结果。这意味着总延迟等于最慢的那个 Hook 的延迟,而非所有 Hook 延迟之和。但这也带来了约束:Hook 之间不能依赖执行顺序,不能假设某个 Hook 先于另一个完成,也不能通过内存共享状态(只能通过文件系统或外部存储)。当多个 Hook 返回冲突的决策时(例如一个返回 allow,另一个返回 deny),系统采用最严格优先的合并策略——deny 优先于 askask 优先于 allow

SessionStart 的特殊能力

在所有事件中,SessionStart 拥有一个独特的机制:环境变量持久化。系统会为每个会话创建一个临时文件,其路径通过 $CLAUDE_ENV_FILE 环境变量传递给 Hook 脚本。Hook 可以向这个文件写入 export KEY=VALUE 格式的行,这些环境变量会在整个会话期间持续生效——不仅对后续的 Hook 可见,对代理循环中执行的所有 Bash 命令也可见。

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
cd "$CLAUDE_PROJECT_DIR" || exit 1

if [ -f "package.json" ]; then
echo "export PROJECT_TYPE=nodejs" >> "$CLAUDE_ENV_FILE"
echo "export NODE_VERSION=$(node -v 2>/dev/null)" >> "$CLAUDE_ENV_FILE"
elif [ -f "Cargo.toml" ]; then
echo "export PROJECT_TYPE=rust" >> "$CLAUDE_ENV_FILE"
echo "export RUST_VERSION=$(rustc --version 2>/dev/null)" >> "$CLAUDE_ENV_FILE"
fi

这个机制使得 SessionStart Hook 可以充当环境探测器——在会话开始时自动检测项目类型、运行时版本、可用工具链,并将这些信息以环境变量的形式持久化。后续的 Hook 和 Bash 命令可以直接读取这些变量来调整行为,而不需要每次都重新检测。


多代理编排与 Worktree 隔离

Claude Code 的多代理系统不是简单的"调用多个 API",而是一套完整的分布式代理协作框架,支持并行执行、模型分级、Worktree 隔离和后台任务。

Agent 工具族与通信模型

工具 功能 通信方式 关键参数
Agent / SubAgent 创建子代理 同步返回结果 model, isolation, name
SendMessage 向已有代理发消息 同步,自动恢复已停止的代理 to: agentId
TaskCreate 创建后台任务 异步,输出写入文件 父代理通过 Read 获取结果

v2.1.77 移除了 Agent 工具的 resume 参数,改为通过 SendMessage({to: agentId}) 恢复已停止的代理。v2.1.59 改进了子代理——在权限被拒绝后继续工作,允许尝试替代方案。

模型分级策略

code-review 插件的源码可以看到精细的模型分级:

模型 适用场景 成本/速度 配置方式
haiku 快速预检(检查 PR 状态、收集文件列表) 最低/最快 model: haiku
sonnet 标准任务(生成摘要、合规审查) 中等 model: sonnet(默认)
opus 复杂分析(Bug 扫描、深度代码分析) 最高/最慢 model: opus
inherit 继承父代理的模型 取决于父代理 model: inherit

v2.1.68 起,Opus 4.6 默认使用 medium effort,可通过 /efforteffort frontmatter 调整。v2.1.72 简化了 effort 级别为 low/medium/high(移除了 max),对应符号 ○ ◐ ●

Worktree 隔离

v2.1.49 引入了 --worktree-w)标志和 isolation: "worktree" 代理属性,允许代理在隔离的 git worktree 中工作。

Worktree 隔离解决了一个核心问题:多个代理同时修改同一个仓库的文件会产生冲突。通过为每个代理创建独立的 worktree,它们可以在不同的分支上并行工作。

技术细节:

  • 稀疏检出:v2.1.76 引入了 worktree.sparsePaths 设置,支持在大型 monorepo 中只检出需要的目录(git sparse-checkout),显著减少 worktree 创建时间和磁盘占用
  • 自动清理:v2.1.76 改进了 stale worktree 的自动清理——中断的并行运行留下的 worktree 会被自动清理
  • CWD 修正:v2.1.77 修复了 worktree 中恢复会话时 cwd 不正确的问题
  • Hook 集成:Worktree 信息(name, path, branch, originalRepoDir)会传递给 Hook 和 statusline 脚本
  • 自定义 VCS 初始化WorktreeCreate/WorktreeRemove Hook 允许在 worktree 创建/删除时执行自定义逻辑(如初始化 Jujutsu VCS)
  • Windows 兼容:v2.1.69 修复了 Windows 上 worktree 文件复制的问题

Agent Teams(代理团队)

Agent Teams 是更高级的多代理模式——多个代理以 tmux 分屏的形式并行运行,共享同一个终端界面。v2.1.32 作为研究预览引入(需要设置 CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)。

从 CHANGELOG 中可以看到 Agent Teams 的技术挑战:

  • 环境传播:v2.1.45 修复了 Agent Teams 在 Bedrock/Vertex/Foundry 上的失败——需要将 API provider 环境变量传播到 tmux 进程
  • 嵌套防护:v2.1.77 修复了 teammate 意外通过 Agent 工具的 name 参数生成嵌套 teammate 的问题
  • 模型继承:v2.1.77 修复了 team agents 继承 leader 模型的问题
  • 内存管理:v2.1.69 修复了 in-process teammates 的完整对话历史被 pin 住,阻止 GC
  • 导航简化:v2.1.47 简化了 teammate 导航——只使用 Shift+Down(带循环),而非 Shift+Up/Down

编排模式实例

模式一:阶段式编排(feature-dev 7 阶段流水线)

模式二:多轮验证流水线(code-review 9 步)

关键设计决策:

  1. 两轮验证:Step 4 发现问题,Step 5 用独立代理验证。这种"发现-验证"模式显著降低误报率。
  2. 严格的信号过滤:明确列出了不应标记的问题类型——预存在问题、Linter 能捕获的问题、主观建议、看起来像 bug 但实际正确的代码。
  3. Agent 3 vs Agent 4 的分工:Agent 3 只看 diff(快速扫描明显 bug),Agent 4 读取上下文(深度分析逻辑问题)。两者并行运行,互不干扰。

MCP 集成架构

MCP(Model Context Protocol) 是 Anthropic 主导的开放协议,定义了 LLM 应用与外部工具/数据源之间的标准通信方式。Claude Code 对 MCP 的集成不是简单的"调用 API",而是一套完整的服务器生命周期管理、认证、工具注册和容错系统。

四种传输方式与通信流程

1. stdio 传输(本地工具)

stdio 是最常用的传输方式,适用于本地 NPM 包和命令行工具。Claude Code 通过 spawn 启动 MCP 服务器进程,然后通过 stdin/stdout 进行 JSON-RPC 双向通信。连接建立后,Claude Code 首先发送 initialize 请求获取服务器的 capabilities(支持哪些功能),然后发送 tools/list 获取可用工具列表。在代理循环中,每次需要调用 MCP 工具时,Claude Code 发送 tools/call 请求并等待响应。会话结束时发送 shutdown 请求并终止进程。

配置示例:

1
2
3
4
5
6
7
8
9
10
{
"mcpServers": {
"github": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "..." }
}
}
}

stdio 传输的进程管理是一个重要的工程问题。MCP 服务器的启动被设计为非阻塞的——服务器在后台启动,不会阻塞 REPL 的渲染,用户可以立即开始输入。启动超时可通过 MCP_TIMEOUT 环境变量配置。进程的生命周期管理需要特别小心:服务器超时后子进程必须被正确 kill,否则会导致 UI 冻结;退出 Claude Code 后所有 stdio 服务器进程必须被清理,否则会产生僵尸进程。安全方面,CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 环境变量可以从 MCP stdio 服务器的环境中剥离 Anthropic 和云提供商的凭证,防止恶意 MCP 服务器窃取 API 密钥。

2. SSE 传输(云服务)

SSE(Server-Sent Events)传输适用于需要 OAuth 认证的云端 MCP 服务。与 stdio 的双向管道不同,SSE 使用 HTTP 的单向推送通道:Claude Code 首先通过 GET /sse 建立一个长连接,服务器通过这个连接推送事件(包括 endpoint URL);Claude Code 随后通过 POST /message 发送 JSON-RPC 请求,服务器的响应通过 SSE 连接异步推送回来。

SSE 传输的核心挑战是连接稳定性。网络波动、服务器重启、负载均衡器超时都可能导致 SSE 连接断开。Claude Code 实现了自动重连机制——连接断开时自动重新建立 SSE 连接。但重连后需要清理旧的工具和资源缓存,否则会导致内存持续增长。另一个棘手的问题是工具调用中途断连:如果 SSE 连接在 tools/call 请求发出后、响应返回前断开,且重连尝试耗尽,工具调用会无限挂起。对于不需要认证的 HTTP/SSE 服务器,系统跳过 OAuth 流程,节省约 600ms 的启动时间。

3. HTTP 传输(无状态 REST)

HTTP 传输是最简单的模式——每次请求都是独立的 POST /mcp,不维护长连接,天然适合 Serverless 后端和 REST API。它的优势是无状态、易于扩展、不需要处理连接管理,但缺点是不支持服务器主动推送。

4. WebSocket 传输(双向实时)

WebSocket 提供全双工通信,适用于需要服务器主动推送的场景(如实时通知、流式输出)。与 SSE 的单向推送不同,WebSocket 允许双方随时发送消息。但 WebSocket 的连接管理更复杂——重连时需要避免 duplicate control_response 消息导致 API 400 错误,listener 的注册和清理也需要特别小心以避免内存泄漏。

OAuth 认证流程

当 Claude Code 尝试连接一个需要认证的 MCP 服务器时,服务器会返回 401 Unauthorized。此时 Claude Code 启动 OAuth 2.0 认证流程。

第一步是发现授权服务器。Claude Code 向 MCP 服务器发送 GET /.well-known/oauth-protected-resource(遵循 RFC 9728 Protected Resource Metadata 规范),服务器返回其关联的授权服务器地址。然后 Claude Code 向授权服务器发送 GET /.well-known/oauth-authorization-server 获取完整的 Authorization Server Metadata(包括 token endpoint、authorization endpoint、支持的 grant types 等)。如果标准发现失败,可以通过 oauth.authServerMetadataUrl 配置指定自定义的发现 URL。

第二步是客户端注册。这里有三种策略:如果授权服务器支持 Dynamic Client Registration(DCR),Claude Code 会自动通过 POST /register 注册自己为客户端,获取 client_idclient_secret;如果服务器不支持 DCR(如 Slack),用户需要通过 --client-id--client-secret 参数预配置凭证;第三种是 CIMD(Client ID Metadata Document,SEP-991 规范)——Claude Code 通过 GET 请求获取一个静态的 Client ID Metadata Document,无需动态注册。

第三步是用户授权。Claude Code 打开系统浏览器导航到授权页面,用户在浏览器中完成授权后,授权服务器通过回调 URL 将 authorization_code 返回给 Claude Code 本地启动的 HTTP 服务器。这里有一个工程细节:回调端口可能被其他程序占用,导致认证挂起——系统需要处理端口冲突的情况。

第四步是 Token 交换与刷新。Claude Code 用 authorization_code 向授权服务器的 token endpoint 换取 access_tokenrefresh_token。Token 存储在 macOS Keychain 或平台等效的受保护存储中。当 access_token 过期时,系统自动使用 refresh_token 获取新的 access_token。这里有一个并发问题:当多个并行的工具调用同时发现 token 过期时,它们可能同时尝试刷新,从 keychain 缓存中读取到同一个过期的 token。系统需要通过锁机制确保只有一个刷新请求被发出。另一个问题是 macOS Keychain 的存储容量——当使用多个 OAuth MCP 服务器时,大型的 OAuth metadata blob 可能溢出 security -i 的 stdin buffer,导致 Keychain 损坏。

最后是 Step-up Authorization。某些操作可能需要比初始授权更高的权限。当 MCP 服务器返回 403 insufficient_scope 时,Claude Code 会重新启动授权流程,请求更高的权限范围。用户需要在浏览器中重新授权,整个过程对代理循环是透明的——工具调用会被暂停,等待重新授权完成后自动重试。

工具注册与动态更新

MCP 服务器连接成功后,Claude Code 通过 tools/list 获取服务器提供的工具列表并注册到代理的可用工具集中。每个工具的描述被限制在 2KB 以内——这是因为很多 MCP 服务器是从 OpenAPI 规范自动生成的,工具描述可能非常冗长,不截断会严重膨胀上下文。注册完成后,系统检查所有 MCP 工具描述的总大小是否超过上下文窗口的 10%——如果超过,则自动启用 ToolSearch 延迟加载,只在系统提示中注入内置工具和 ToolSearch 工具,其余 MCP 工具在需要时通过 ToolSearch 动态发现。

MCP 协议支持 list_changed 通知——服务器可以在运行时动态更新其可用工具、prompts 和 resources,无需断开连接。当 Claude Code 收到这个通知时,会重新获取工具列表并更新内部注册表。去重也是一个重要的工程问题:当本地配置和 claude.ai connector 配置了相同的 MCP 服务器时,本地配置优先;插件提供的 MCP 服务器与手动配置的服务器(相同 command 或 URL)重复时自动跳过。

Elicitation(结构化输入请求)

Elicitation 是 MCP 协议中一个独特的能力——它允许 MCP 服务器在工具执行过程中主动向用户请求结构化输入。例如,一个数据库 MCP 服务器在执行危险操作前可能需要用户确认,或者一个部署服务器需要用户选择目标环境。服务器发送 elicitation 请求(包含表单字段定义),Claude Code 向用户显示交互式对话框或打开浏览器 URL,用户填写完成后将结果返回给服务器,服务器继续执行工具调用。Elicitation 有对应的 Hook 事件,允许外部系统拦截和自动回答 elicitation 请求——这在 CI/CD 环境中尤其有用,可以避免人工干预。

Channels(消息推送)

Channels 是一个研究预览功能(通过 --channels 参数启用),它反转了传统的 MCP 通信方向——不是 Claude Code 调用 MCP 服务器,而是 MCP 服务器主动向会话推送消息。这使得外部系统可以实时向代理注入信息——例如来自手机的审批结果、来自 CI 系统的构建状态、来自监控系统的告警。Channel 服务器还可以声明 permission capability,将工具审批提示转发到手机——这意味着用户可以在手机上审批代理的工具调用,而不需要坐在电脑前。当 Channels 激活时,系统会禁用 AskUserQuestion 和 plan-mode 工具,因为用户交互已经通过 Channel 进行。

二进制内容处理

MCP 工具可能返回二进制内容——PDF、Office 文档、音频、图片等。将这些内容以 base64 编码形式放入对话上下文会浪费大量 token。Claude Code 的处理策略是自动将二进制内容保存到磁盘,使用正确的文件扩展名,然后在上下文中只保留文件路径引用和内容摘要。WebFetch 工具也采用相同的策略——二进制响应保存到磁盘并附上摘要。此外,MCP 协议的 resource_link 类型工具结果也被支持,允许服务器返回对外部资源的引用而非实际内容。


安全沙箱模型

Claude Code 实现了五层纵深安全架构

第一层:权限规则系统

三级控制:allow(自动允许)、ask(询问用户)、deny(拒绝)。

1
2
3
4
5
6
7
{
"permissions": {
"allow": ["Read", "Bash(git *)"],
"ask": ["Write", "Edit", "Bash"],
"deny": ["WebSearch", "WebFetch"]
}
}

权限规则的优先级链从高到低依次是:deny 规则具有最高优先级,任何其他规则都无法覆盖(包括 Hook 返回的 allow);其次是企业 managed policy 的 ask 规则(不能被用户的 allow 规则覆盖);然后是用户的 allow 规则;再然后是 Skill 的 allowed-tools 声明;最后是默认行为。这个优先级链确保了安全约束不会被低优先级的配置意外放宽。权限系统还需要防御各种绕过尝试——例如通过 shell 行续接(line continuation)拆分危险命令,或通过通配符权限规则匹配包含 shell 操作符的复合命令。

第二层:文件系统沙箱

macOS 使用 Seatbelt(系统级沙箱),Linux 使用自定义沙箱实现。沙箱通过配置文件声明文件系统的读写权限——allowWrite 指定允许写入的目录,denyRead 指定禁止读取的文件,excludedCommands 指定不受沙箱限制的命令(如 ghterraform 等需要广泛文件访问的工具)。一个关键的设计决策是 failIfUnavailable 参数——当设为 true 时,如果沙箱启用但无法启动(例如依赖缺失),系统会直接退出而非静默降级为无沙箱模式。沙箱还需要防御符号链接攻击——通过符号链接父目录写入新文件可以逃逸工作目录限制。

1
2
3
4
5
6
7
8
9
10
11
{
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"filesystem": {
"allowWrite": ["/tmp/claude/"],
"denyRead": ["/etc/shadow"]
},
"excludedCommands": ["gh", "terraform"]
}
}

第三层:网络防火墙(DevContainer)

init-firewall.sh 实现了基于 iptables + ipset 的严格网络白名单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 默认策略:DROP 所有流量
iptables -P INPUT DROP && iptables -P FORWARD DROP && iptables -P OUTPUT DROP

# 动态获取 GitHub IP 段并加入白名单
gh_ranges=$(curl -s https://api.github.com/meta)
echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | while read -r cidr; do
ipset add allowed-domains "$cidr"
done

# DNS 解析白名单域名
for domain in "registry.npmjs.org" "api.anthropic.com" "sentry.io"; do
dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}' | while read -r ip; do
ipset add allowed-domains "$ip"
done
done

# 仅允许白名单流量
iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT
iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited

v2.1.69 引入了 sandbox.enableWeakerNetworkIsolation(macOS only),允许 Go 程序在使用 MITM 代理时验证 TLS 证书。

第四层:凭证保护

凭证保护解决的是一个微妙但关键的安全问题:代理执行的工具(包括 MCP 服务器和 Bash 命令)可能读取环境变量中的 API 密钥。CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 环境变量会从所有子进程的环境中剥离 Anthropic 和云提供商的凭证,防止恶意 MCP 服务器窃取 API 密钥。插件选项系统中标记为 sensitive: true 的值(如 API key)存储在 macOS Keychain 或平台等效的受保护存储中,而非明文配置文件。系统还需要确保敏感数据(OAuth tokens、API keys、passwords)不会在 debug 日志中暴露。

第五层:企业策略管控

配置采用 7 层覆盖机制

1
2
3
4
5
6
7
8
9
10
11
12
13
企业管理配置 (managed-settings.json / managed-settings.d/*.json)
↓ 覆盖
用户全局配置 (~/.claude/settings.json)
↓ 覆盖
用户本地配置 (~/.claude/settings.local.json)
↓ 覆盖
项目配置 (.claude/settings.json)
↓ 覆盖
项目本地配置 (.claude/settings.local.json)
↓ 覆盖
环境变量
↓ 覆盖
CLI 参数

v2.1.83 引入了 managed-settings.d/ 分片目录,允许企业通过多个配置文件组合管理策略。配置来源不限于 JSON 文件——macOS 支持 plist,Windows 支持 Registry,方便与现有的企业设备管理系统(MDM)集成。

企业管理员可以通过这套机制控制几乎所有方面的行为:enabledPluginsallowedChannelPlugins 控制插件白名单;strictKnownMarketplaces 配合 hostPattern/pathPattern 限制插件只能从指定的仓库安装;deniedMcpServersallowedMcpServers 控制 MCP 服务器的黑白名单;disableAllHooks 禁用所有用户定义的 Hook(但 managed hooks 不受影响);permissions.disableBypassPermissionsMode 禁止用户绕过权限模式;allowManagedDomainsOnly 让沙箱自动阻止非白名单域名的网络访问,无需用户确认。


性能工程

Claude Code 在性能方面的工程深度主要体现在三个方面:启动性能、长会话内存管理和 Prompt Cache 命中率优化。

启动性能

启动性能的核心策略是延迟加载和并行化。几乎所有非必要的初始化工作都被延迟到首次使用时:Yoga WASM 布局引擎的预加载被延迟(节省 ~16MB 内存),Zod schema 的构建被延迟(减少事件循环阻塞),原生图片处理器被延迟到首次处理图片时才加载,SessionStart Hook 的执行被延迟(节省 ~500ms)。并行化也被广泛应用:macOS Keychain 凭证的读取被并行化(节省 ~60ms),setup() 与 slash command 加载并行执行,claude.ai 的 MCP 配置获取与本地连接并行进行,MCP 工具的 token 计数被批量合并为单个 API 调用。--bare -p 模式是极致优化的体现——它禁用 auto-memory、hooks、LSP、plugin sync 和 skill directory walks,使得到达 API 请求的时间减少约 14%。

内存管理

长会话的内存管理是一个持续的工程挑战。Claude Code 使用 React Compiler 渲染 TUI,这意味着每次对话更新都会触发重新渲染,而旧的消息数组版本可能在 React 的 memoCache 中累积。REPL 的 render scopes 也会随对话轮次增长,大约每 1000 轮累积 35MB。Yoga WASM 布局引擎的线性内存会无限增长,需要定期重置 tree-sitter parser。MCP 服务器重连时工具和资源缓存需要被清理,否则内存会持续增长。Streaming buffer 在 generator 提前终止时需要通过 finally 块释放。文件历史快照需要设置上限防止无限增长。CircularBuffer 在 clear() 后 backing array 仍然保留引用,需要显式重置。Git diff 解析中的 sliced strings 会保留对大型 parent strings 的引用,需要复制字符串来断开引用链。Agent Teams 中 Teammate 的完整对话历史需要在 Compaction 后正确释放。长会话中 Hook 事件会无限累积,需要设置上限。这些问题的共同特点是:它们在短会话中不可见,只有在持续数小时的长会话中才会显现。

Prompt Cache 优化

Anthropic 的 Prompt Cache 机制允许 API 请求中前缀匹配的 token 命中缓存,缓存 token 的计费仅为正常输入的 1/10,且延迟显著降低。对于代理循环场景,每一轮都会发送完整的 system prompt + 历史消息 + 新消息,system prompt 的稳定性直接决定了缓存命中率。Claude Code 的代理循环可能在一个会话中执行数十到数百轮 API 调用,cache 命中率的差异会导致成本相差数倍。

围绕这个核心问题,Claude Code 做了四个层面的优化:

系统提示去动态化。 最直接的策略是消除 system prompt 中的一切动态内容。日期信息从系统提示中移出,工具描述中的动态内容被剥离(这对 Bedrock/Vertex/Foundry 等非直连 API 用户尤为重要,因为这些平台的 cache 机制对 prompt 变化更敏感)。@ 文件引用的原始字符串内容不再进行 JSON 转义——JSON 转义会将换行符、引号等字符转换为转义序列,改变 token 边界,导致不必要的 cache miss。这些看似微小的改动,累积起来对长会话的 cache 命中率有显著影响。

精确的 Cache 失效控制。 Cache 失效的粒度需要精确把控。早期实现中 cache 仅在工具名称变更时失效,但工具描述和 schema 变更时不会触发失效,导致模型使用过时的工具定义产生错误调用。修复后的策略是对工具的名称、描述、schema 三个维度做哈希比对,任一变化都触发失效。MCP 服务器带 instructions 时需要特殊处理——如果 MCP 服务器在首轮之后才连接并携带 instructions,会导致系统提示结构变化,cache 全部失效。解决方案是在系统提示中为 MCP instructions 预留固定的插槽位置,即使内容为空也保持结构一致。Prompt suggestion 的 cache 也经历了多次回归修复,核心问题是 suggestion 的生成依赖上下文状态,状态变化时 suggestion 缓存需要同步失效,但失效范围不能过大。

ToolSearch 与 Cache 的兼容。 ToolSearch 延迟加载工具时,动态注入的工具 schema 会改变 prompt 结构,破坏 cache 前缀匹配。解决方案是将全局系统提示与工具 schema 分层缓存——全局系统提示作为第一层 cache breakpoint,工具 schema 作为第二层,这样即使工具列表因 ToolSearch 动态变化,系统提示部分的 cache 仍然可以命中。

Compaction 中的 Cache 复用。 Compaction 压缩历史消息时,压缩请求本身也是一次 API 调用。通过在压缩请求中保留图片等大体积内容,可以复用已有的 prompt cache,使压缩操作本身更快更便宜。SDK 层面的 query() 调用也需要正确处理 cache——早期实现中 SDK 调用会不正确地使 cache 失效,修复后输入 token 成本降低最高可达 12 倍。

这些优化的累积效果是显著的:对于典型的长会话(数十轮代理循环),cache 命中率从早期的不稳定状态提升到了稳定的高水平,p90 prompt cache rate 得到了实质性改善。考虑到 Claude Code 的使用场景天然是长会话、多轮交互,Prompt Cache 优化可能是所有性能优化中 ROI 最高的一项。


总结

Claude Code 的技术架构可以用四个关键词概括:

  1. 上下文精算:从 Skill 的三级渐进式加载、MEMORY.md 的 25KB/200 行截断、工具结果的 50K 磁盘持久化、MCP 工具描述的 2KB 限制、ToolSearch 的动态延迟加载,到 Auto-Compaction 的熔断机制和 Partial Summarization 的精确控制——整个系统的设计都围绕着"每一个 token 都有成本"的原则。

  2. 可编程性:15+ 种 Hook 事件 × 3 种 Hook 类型(command/prompt/http)× 条件触发(if 字段)× 并行执行 × once: true 一次性触发,构成了一个完整的事件驱动编程模型。Hook 不仅能拦截和修改代理行为,还能通过 $CLAUDE_ENV_FILE 持久化环境变量、通过 updatedInput 修改工具参数、通过 systemMessage 注入上下文、通过 additionalContext 提供额外信息。

  3. 多代理编排:模型分级(Haiku/Sonnet/Opus)× Worktree 隔离(稀疏检出 + 自动清理)× 后台任务(5GB 输出限制 + 30K 截断)× Agent Teams(tmux 分屏)× 两轮验证模式,构成了一套完整的分布式代理协作框架。

  4. 协议驱动:MCP 集成不是简单的 API 调用,而是一套完整的服务器生命周期管理系统——4 种传输方式、OAuth 2.0 全流程(RFC 9728 发现 + DCR/CIMD 注册 + Step-up Auth)、list_changed 动态工具更新、Elicitation 结构化输入、Channels 消息推送、二进制内容智能处理。