0%

Claude Code源码分析

基于 Claude Code v2.x 源码的完整技术分析文档。涵盖入口启动、代理循环、工具系统、权限安全、上下文管理、记忆系统、多代理架构、MCP 协议集成、遥测安全等九大模块。

昨天刚刚update完分析Claude code架构的文章,就爆出来Claude code源码泄露,趁这个热度,我们借助Claude 4.6 opus的力量,剖析一下技术架构

一、入口与启动流程

Claude Code 的启动链路从 src/entrypoints/cli.tsx 开始,经过多层快速路径分发、初始化、认证、配置加载,最终进入 REPL 交互循环或 headless 执行模式。整个设计的核心原则是延迟加载——只在需要时才加载模块,最小化冷启动时间。

启动入口:cli.tsx 的快速路径分发

src/entrypoints/cli.tsx 是整个应用的 bootstrap 入口。它的设计哲学是零导入快速路径——所有 import 都是动态的(await import(...)),确保不匹配的路径不会加载任何多余模块。

顶层副作用(在 main() 之前执行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 禁用 corepack 自动 pin(防止污染用户的 package.json)
process.env.COREPACK_ENABLE_AUTO_PIN = '0'

// 2. CCR 容器环境设置 8GB 堆上限
if (process.env.CLAUDE_CODE_REMOTE === 'true') {
process.env.NODE_OPTIONS = `${existing} --max-old-space-size=8192`
}

// 3. L0 消融基线(Ant-only,feature-gated DCE)
// 同时禁用 thinking、compact、auto-memory、background tasks
if (feature('ABLATION_BASELINE') && process.env.CLAUDE_CODE_ABLATION_BASELINE) {
for (const k of ['CLAUDE_CODE_SIMPLE', 'CLAUDE_CODE_DISABLE_THINKING', ...]) {
process.env[k] ??= '1'
}
}

消融基线必须在 cli.tsx 而非 init.ts 中设置——因为 BashTool/AgentTool/PowerShellTool 在模块导入时就将 DISABLE_BACKGROUND_TASKS 捕获为模块级常量,init() 运行时已经太晚。

快速路径分发树

main() 函数按优先级检查命令行参数,匹配到的路径立即执行并返回,不加载完整 CLI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cli.tsx main()
├── --version / -v / -V → 打印版本号,零导入
├── --dump-system-prompt → 输出渲染后的 system prompt(Ant-only)
├── --claude-in-chrome-mcp → Chrome 扩展 MCP 服务器
├── --chrome-native-host → Chrome Native Messaging Host
├── --computer-use-mcp → Computer Use MCP 服务器(CHICAGO_MCP)
├── --daemon-worker=<kind> → Daemon Worker 进程(DAEMON)
├── remote-control / rc / bridge → Bridge 远程控制模式(BRIDGE_MODE)
│ ├── OAuth 认证检查
│ ├── GrowthBook 门控检查
│ ├── 最低版本检查
│ └── 企业策略 allow_remote_control 检查
├── daemon [subcommand] → Daemon 长驻进程(DAEMON)
├── ps / logs / attach / kill → 后台会话管理(BG_SESSIONS)
│ └── --bg / --background
├── new / list / reply → 模板任务命令(TEMPLATES)
├── environment-runner → BYOC 环境运行器(BYOC_ENVIRONMENT_RUNNER)
├── self-hosted-runner → 自托管运行器(SELF_HOSTED_RUNNER)
├── --worktree --tmux → Tmux Worktree 快速路径
├── --bare → 设置 CLAUDE_CODE_SIMPLE=1
└── [默认] → 加载完整 CLI(main.tsx)
├── startCapturingEarlyInput() // 捕获用户在加载期间的输入
└── import('../main.js').main()

设计要点

  1. --version 零导入:只使用编译时内联的 MACRO.VERSION,不加载任何模块。这是最快的路径。
  2. Bridge 模式的认证前置:OAuth 检查必须在 GrowthBook 门控之前——没有认证,GrowthBook 没有用户上下文,会返回过期的默认值 false
  3. --bare 的早期设置:在加载完整 CLI 之前设置 CLAUDE_CODE_SIMPLE=1,确保模块求值期间的门控就能生效。
  4. startCapturingEarlyInput():在加载 main.tsx(~789KB)期间捕获用户的键盘输入,避免输入丢失。

init() 初始化函数

src/entrypoints/init.ts 中的 init() 是核心初始化函数,使用 memoize 确保只执行一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
init() 执行序列

├── enableConfigs() // 验证并启用配置系统
├── applySafeConfigEnvironmentVariables() // 应用安全的环境变量(trust 之前)
├── applyExtraCACertsFromConfig() // 加载自定义 CA 证书(必须在首次 TLS 握手前)
├── setupGracefulShutdown() // 注册优雅退出处理

├── [异步] initialize1PEventLogging() // 初始化第一方事件日志
│ └── onGrowthBookRefresh() → reinitialize1PEventLoggingIfConfigChanged()
├── [异步] populateOAuthAccountInfoIfNeeded() // 填充 OAuth 账户信息
├── [异步] initJetBrainsDetection() // JetBrains IDE 检测
├── [异步] detectCurrentRepository() // GitHub 仓库检测

├── initializeRemoteManagedSettingsLoadingPromise() // 远程托管设置
├── initializePolicyLimitsLoadingPromise() // 策略限制
├── recordFirstStartTime() // 记录首次启动时间

├── configureGlobalMTLS() // mTLS 配置
├── configureGlobalAgents() // HTTP 代理配置
├── preconnectAnthropicApi() // 预连接 Anthropic API(TCP+TLS 握手重叠)

├── [CCR] initUpstreamProxy() // 上游代理(容器环境)
├── setShellIfWindows() // Windows git-bash 设置
├── registerCleanup(shutdownLspServerManager) // LSP 清理注册
├── registerCleanup(cleanupSessionTeams) // Swarm 团队清理注册
└── ensureScratchpadDir() // 创建 Scratchpad 临时目录

关键设计细节

  1. 安全环境变量分层applySafeConfigEnvironmentVariables() 只应用不需要 trust 的变量(如 NODE_EXTRA_CA_CERTS)。完整的环境变量(applyConfigEnvironmentVariables())在 trust dialog 之后才应用——防止不受信任的项目配置在用户同意前生效。

  2. CA 证书的时序约束applyExtraCACertsFromConfig() 必须在 configureGlobalAgents() 之前执行。Bun 通过 BoringSSL 在启动时缓存 TLS 证书存储,如果在首次 TLS 握手之后才加载自定义 CA,连接会失败。

  3. API 预连接preconnectAnthropicApi() 在 CA 证书和代理配置完成后执行,将 TCP+TLS 握手(~100-200ms)与后续的 action handler 工作(~100ms)重叠。对于 proxy/mTLS/unix/cloud-provider 场景跳过——SDK 的 dispatcher 不会复用全局连接池。

  4. 1P 事件日志的延迟初始化:通过 Promise.all([import('firstPartyEventLogger'), import('growthbook')]) 延迟加载 OpenTelemetry sdk-logs 模块。由于 growthbook.js 此时已在模块缓存中(被 firstPartyEventLogger 导入过),第二次动态导入零成本。

main.tsx 的完整启动序列

src/main.tsx(789KB,最大文件)包含完整的 CLI 启动逻辑。以下是从 main() 到 REPL 渲染的关键阶段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
main.tsx main() 启动序列

├── 1. 早期解析
│ ├── eagerLoadSettings() // 解析 --settings、--setting-sources
│ ├── initializeEntrypoint() // 设置 CLAUDE_CODE_ENTRYPOINT
│ └── eagerParseCliFlag() // 提取关键 CLI 标志

├── 2. Commander 参数解析
│ ├── program.option('--model')
│ ├── program.option('--permission-mode')
│ ├── program.option('--mcp-config')
│ ├── program.option('--agents')
│ └── ... (~40 个选项)

├── 3. init() 调用
│ └── 见上文 init() 序列

├── 4. 并行预取(用户还在输入时)
│ ├── initUser() // 用户信息
│ ├── getUserContext() // 用户上下文
│ ├── prefetchSystemContextIfSafe() // 系统上下文
│ ├── getRelevantTips() // 提示信息
│ ├── countFilesRoundedRg() // 文件计数(ripgrep)
│ ├── initializeAnalyticsGates() // 分析门控
│ ├── prefetchOfficialMcpUrls() // MCP URL 预取
│ ├── refreshModelCapabilities() // 模型能力刷新
│ ├── settingsChangeDetector.initialize() // 设置变更检测
│ └── skillChangeDetector.initialize() // 技能变更检测

├── 5. MCP 配置加载(与 setup 并行)
│ ├── getClaudeCodeMcpConfigs() // 本地 MCP 配置
│ ├── fetchClaudeAIMcpConfigsIfEligible() // claude.ai MCP 配置
│ └── filterMcpServersByPolicy() // 企业策略过滤

├── 6. Setup 流程
│ ├── showSetupScreens() // 首次运行引导
│ │ ├── Trust Dialog // 工作区信任确认
│ │ ├── OAuth / API Key 认证 // 认证流程
│ │ └── Onboarding // 新用户引导
│ ├── initializeTelemetryAfterTrust() // Trust 后初始化遥测
│ └── applyConfigEnvironmentVariables() // 应用完整环境变量

├── 7. 后 Trust 初始化
│ ├── loadRemoteManagedSettings() // 加载远程托管设置
│ ├── initializeLspServerManager() // LSP 服务器管理器
│ ├── launchInvalidSettingsDialog() // 设置验证错误对话框
│ └── 后台预取(quota、passes、fastMode、bootstrap)

├── 8. 模型与命令加载
│ ├── getCommands() // 加载 ~80 个斜杠命令
│ ├── getAgentDefinitionsWithOverrides() // 加载代理定义
│ └── 模型解析(--model → alias resolution)

├── 9. MCP 连接
│ ├── prefetchAllMcpResources() // 预取 MCP 资源
│ └── 连接所有配置的 MCP 服务器

└── 10. 渲染
├── createRoot() // 创建 Ink 渲染根
└── root.render(<REPL />) // 渲染 REPL 组件
或 runHeadless() // headless 模式执行

入口点类型识别

initializeEntrypoint() 根据运行环境设置 CLAUDE_CODE_ENTRYPOINT

入口点 条件 说明
'mcp' claude mcp serve MCP 服务器模式
'claude-code-github-action' CLAUDE_CODE_ACTION=true GitHub Action
'local-agent' 预设的环境变量 本地代理模式
'sdk-cli' 非交互式(-p 参数) SDK/headless 模式
'cli' 交互式(默认) 终端 REPL 模式

四种运行模式

REPL 模式:完整的 React/Ink TUI,包括输入框、消息列表、虚拟滚动、权限对话框、状态栏等。通过 root.render(<REPL />) 启动。

Headless 模式-p):QueryEnginesrc/QueryEngine.ts)管理查询生命周期。不渲染 UI,直接输出结果。支持 --output-format json 结构化输出。

SDK 模式:通过 structuredIOsrc/cli/structuredIO.ts)与宿主进程通信。VS Code 扩展、Claude Desktop 等通过此模式集成。

MCP Server 模式src/entrypoints/mcp.ts 将 Claude Code 自身作为 MCP 服务器暴露,允许其他 MCP 客户端调用其工具。

配置加载层级

配置文件按优先级从低到高加载:

层级 路径 说明
1. 默认值 硬编码 内置默认配置
2. 全局 ~/.claude/settings.json 用户全局设置
3. 本地全局 ~/.claude/settings.local.json 用户本地设置(gitignored)
4. 企业 /etc/claude-code/settings.json 企业管理员设置
5. 项目 .claude/settings.json 项目级设置(checked in)
6. 项目本地 .claude/settings.local.json 项目本地设置(gitignored)
7. CLI 参数 --settings <path> 命令行指定的设置文件
8. 远程托管 API 获取 远程托管设置(每小时轮询)

--setting-sources 参数可以控制加载哪些来源——企业环境中可以只加载 managed 来源,忽略用户和项目设置。

认证流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
认证检查
├── OAuth Token 存在?
│ ├── 是 → 检查 token 有效性
│ │ ├── 有效 → 使用 OAuth
│ │ └── 过期 → 刷新 token
│ │ ├── 刷新成功 → 使用新 token
│ │ └── 刷新失败 → 重新授权
│ └── 否 → 检查 API Key
│ ├── ANTHROPIC_API_KEY 存在 → 使用 API Key
│ ├── Bedrock 配置 → AWS 认证
│ ├── Vertex 配置 → GCP 认证
│ └── 无认证 → 引导用户登录
│ ├── /login 命令
│ └── OAuth 浏览器授权流程

OAuth 优先于 API Key:OAuth token 支持 user:profile scope,可以获取用户信息用于 GrowthBook 定向和遥测。API Key 用户缺少这些能力。

--bare 模式的性能优化

--bare(或 CLAUDE_CODE_SIMPLE=1)是性能优化模式,跳过所有非必要的启动工作:

跳过的内容 节省
后台预取(quota、passes、fastMode) ~200ms 网络
claude.ai MCP 代理服务器 6-14s/服务器
自动发现的 MCP(.mcp.json、用户设置) ~100ms I/O
Skill 目录遍历 ~50ms I/O
设置/技能变更检测器 ~30ms
Auto Memory 后台 LLM 调用
Hooks 子进程开销
LSP 服务器 进程开销
Plugin 同步 网络 I/O

实测到达首次 API 请求的时间减少约 14%。适用于脚本化的 -p 调用——这些调用没有"用户正在输入"的窗口来隐藏预取工作。

启动性能追踪

profileCheckpoint() 在关键节点记录时间戳,用于启动性能分析:

1
2
3
4
5
6
7
8
9
10
cli_entry                    → CLI 入口
init_function_start → init() 开始
init_configs_enabled → 配置系统就绪
init_safe_env_vars_applied → 安全环境变量已应用
init_after_graceful_shutdown → 优雅退出已注册
init_after_1p_event_logging → 1P 日志已初始化
init_network_configured → 网络(mTLS + proxy)已配置
init_function_end → init() 完成
action_commands_loaded → 命令和代理已加载
cli_after_main_complete → main() 完成

tengu_timer 事件在 Ink 根创建后立即记录启动时间——在任何阻塞对话框(trust/OAuth/onboarding/resume-picker)之前。旧的实现在 REPL 首次渲染时记录,p99 达到 ~70s(被对话框等待时间主导)。

事件循环阻塞检测

启动后注册一个事件循环阻塞检测器——当主线程被阻塞超过 500ms 时记录日志。这用于诊断启动卡顿和运行时性能问题。

1
2
// Event loop stall detector — logs when the main thread is blocked >500ms
void initializeEventLoopStallDetector()

二、代理循环(Agent Loop)核心

Claude Code 的心脏是 src/query.ts 中的 query() 异步生成器函数。它实现了一个工具增强的 ReAct 循环:用户输入 → 组装 system prompt → 调用 Claude API → 流式解析响应 → 如果包含 tool_use 则执行工具并将结果追加到消息列表 → 循环回 API 调用,直到模型返回纯文本。

整个循环被封装为一个 AsyncGenerator,通过 yield 向调用者(TUI 渲染层)逐条推送事件流。这个设计使得 UI 可以在流式传输过程中实时渲染模型输出和工具执行进度,而不需要等待完整响应。

循环状态机

query() 函数的核心是一个 while (true) 无限循环,每次迭代代表一次完整的 API 请求-响应-工具执行周期。循环通过一个 State 类型管理跨迭代的可变状态:

1
2
3
4
5
6
7
8
9
10
11
12
type State = {
messages: Message[] // 完整消息历史
toolUseContext: ToolUseContext // 工具执行上下文(工具列表、权限、abort 信号等)
autoCompactTracking: AutoCompactTrackingState // 自动压缩追踪(是否已压缩、轮次计数器)
maxOutputTokensRecoveryCount: number // max_output_tokens 恢复计数(最多 3 次)
hasAttemptedReactiveCompact: boolean // 是否已尝试响应式压缩(防止无限重试)
maxOutputTokensOverride: number | undefined // 输出 token 上限覆盖(升级到 ESCALATED_MAX_TOKENS)
pendingToolUseSummary: Promise<...> // 待处理的工具使用摘要(Haiku 异步生成)
stopHookActive: boolean | undefined // Stop Hook 是否活跃(阻止循环终止)
turnCount: number // 当前轮次计数
transition: Continue | undefined // 上一次迭代的继续原因(用于测试断言恢复路径)
}

设计哲学:State 在每次迭代开头被解构为局部变量(只读),在 continue 站点通过 state = { ... } 整体替换(而非 9 个独立赋值)。这保证了状态转换的原子性——不会出现半更新的中间状态。

循环的终止条件由 Terminal 类型表示,包含 reason 字段标识退出原因:

Terminal Reason 触发条件
'completed' 模型返回纯文本,无 tool_use
'aborted_streaming' 用户在流式传输中按 Ctrl+C
'aborted_tools' 用户在工具执行中按 Ctrl+C
'model_error' API 调用抛出异常(非 413/max_output_tokens)
'prompt_too_long' 413 错误且所有恢复策略均失败
'image_error' 图片大小/格式错误
'max_turns' 达到 maxTurns 限制
'hook_stopped' PreToolUse Hook 返回 preventContinuation
'stop_hook_prevented' Stop Hook 返回 preventContinuation
'blocking_limit' 上下文硬上限阻塞

每次迭代的完整流程

迭代流程较长,拆分为三个阶段展示:预处理 → 流式调用 → 后处理

阶段一:消息预处理与 API 调用
阶段二:流式解析与错误恢复
阶段三:Hook 检查与循环决策

循环入口的双层封装

query() 实际上是一个薄包装器,真正的循环逻辑在 queryLoop() 中:

1
2
3
4
5
6
7
8
9
export async function* query(params: QueryParams): AsyncGenerator<...> {
const consumedCommandUuids: string[] = []
const terminal = yield* queryLoop(params, consumedCommandUuids)
// 只有正常返回时才执行——throw 和 .return() 都会跳过
for (const uuid of consumedCommandUuids) {
notifyCommandLifecycle(uuid, 'completed')
}
return terminal
}

这个双层设计的目的是命令生命周期追踪consumedCommandUuids 收集本轮消费的排队命令(slash commands、task notifications),只有循环正常完成时才标记为 'completed'。如果循环因错误或中断退出,命令保持未完成状态,下次可以重试。

queryLoop 的初始化

循环开始前,queryLoop() 执行以下初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1. 解构不可变参数(循环内不会重新赋值)
const { systemPrompt, userContext, systemContext, canUseTool,
fallbackModel, querySource, maxTurns, skipCacheWrite } = params

// 2. 初始化可变状态
let state: State = {
messages: params.messages,
toolUseContext: params.toolUseContext,
maxOutputTokensOverride: params.maxOutputTokensOverride,
// ... 其余字段初始化为默认值
}

// 3. Token Budget 追踪器(feature-gated)
const budgetTracker = feature('TOKEN_BUDGET') ? createBudgetTracker() : null

// 4. Task Budget 跨压缩追踪
// 未压缩时 undefined(服务端可以看到完整历史自行计算)
// 压缩后递减(服务端只看到摘要,无法计算已消耗量)
let taskBudgetRemaining: number | undefined = undefined

// 5. 快照不可变的环境/statsig/会话状态
const config = buildQueryConfig()

// 6. 启动 Memory Prefetch(每用户轮次一次,使用 `using` 自动清理)
using pendingMemoryPrefetch = startRelevantMemoryPrefetch(
state.messages, state.toolUseContext
)

using 关键字(TC39 Explicit Resource Management 提案)确保 pendingMemoryPrefetch 在生成器退出时(无论正常、异常还是 .return())都会被 dispose,触发遥测记录和资源释放。

System Prompt 的动态组装

src/constants/prompts.ts 中的 getSystemPrompt() 函数负责组装完整的系统提示。它返回一个 string[],每个元素是一个独立的 prompt section。组装分为静态部分动态部分,中间由 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 标记分隔——这个边界是 Prompt Cache 优化的关键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
getSystemPrompt() 组装顺序

├── 🔒 静态内容(scope: global,跨用户可缓存)
│ ├── ① getSimpleIntroSection() — 身份声明 + CYBER_RISK_INSTRUCTION
│ ├── ② getSimpleSystemSection() — 系统行为规则(工具调用规范、XML 标签使用等)
│ ├── ③ getSimpleDoingTasksSection() — 编码任务指导(文件编辑策略、测试要求等)
│ ├── ④ getActionsSection() — 操作安全指导(可逆性评估、爆炸半径控制)
│ ├── ⑤ getUsingYourToolsSection() — 工具使用指导(并行调用、搜索策略等)
│ ├── ⑥ getSimpleToneAndStyleSection() — 语气风格(简洁、专业、不道歉)
│ └── ⑦ getOutputEfficiencySection() — 输出效率(避免冗余、倒金字塔结构)

├── ═══ SYSTEM_PROMPT_DYNAMIC_BOUNDARY ═══ ← Prompt Cache 分界线

└── 🔄 动态内容(scope: session,每次可能变化)
├── ⑧ getSessionSpecificGuidanceSection() — 会话特定指导(恢复会话时的上下文提示)
├── ⑨ loadMemoryPrompt() — CLAUDE.md 四级加载 + memdir 结构化记忆
├── ⑩ getAntModelOverrideSection() — Ant 用户模型覆盖(内部员工专用 prompt)
├── ⑪ computeSimpleEnvInfo() — 环境信息(CWD / OS / Git / 模型名 / 知识截止日期)
├── ⑫ getLanguageSection() — 用户语言偏好
│ + getOutputStyleSection() — 输出风格(Markdown / 代码块偏好)
├── ⑬ getMcpInstructionsSection() — MCP 服务器指令(服务器级 + 工具级)
└── ⑭ getScratchpadInstructions() — Scratchpad 临时目录指引
+ getFunctionResultClearingSection() — 工具结果自动清理提示
+ SUMMARIZE_TOOL_RESULTS — 模型侧结果摘要指令
+ getProactiveSection() — KAIROS 自主代理行为指令(feature-gated)

关键设计细节

  1. CYBER_RISK_INSTRUCTION 被硬编码在 getSimpleIntroSection 的最前面,由 Safeguards 团队维护,定义了安全边界——允许授权安全测试、CTF 挑战,拒绝破坏性技术、DoS 攻击、供应链攻击。这是整个 system prompt 中唯一不可被任何配置覆盖的部分。

  2. Ant 用户差异化process.env.USER_TYPE === 'ant' 分支为 Anthropic 内部员工提供了更详细的 prompt。例如代码风格部分增加了"默认不写注释"、"完成前验证"等指令;输出效率部分替换为更详细的"用户沟通"指南(流畅散文、倒金字塔结构、避免语义回溯)。这个环境变量是 Bun 的 --define 编译时常量——在外部构建中被替换为 false,相关分支被 DCE 消除。

  3. 模型代号体系FRONTIER_MODEL_NAME = 'Claude Opus 4.6',模型 ID 映射为 { opus: 'claude-opus-4-6', sonnet: 'claude-sonnet-4-6', haiku: 'claude-haiku-4-5-20251001' }。源码中有 @[MODEL LAUNCH] 注释标记需要在新模型发布时更新的位置。getKnowledgeCutoff() 为每个模型返回不同的知识截止日期(Opus 4.6: May 2025, Sonnet 4.6: August 2025)。

  4. Undercover 模式:当 isUndercover() 返回 true 时,computeSimpleEnvInfo() 中所有模型名称/ID 引用被抑制,getAntModelOverrideSection() 返回 null。Undercover 模式在非内部仓库中自动激活,防止 Anthropic 员工在公开仓库中泄露内部模型代号。DCE 注释强调 process.env.USER_TYPE === 'ant' 必须在每个调用点内联(不能提升为 const),否则 bundler 无法常量折叠。

  5. Scratchpad 目录getScratchpadInstructions() 为每个会话提供一个隔离的临时目录,替代 /tmp。这避免了多会话间的文件冲突,且不需要权限提示。

  6. Function Result ClearinggetFunctionResultClearingSection() 告知模型旧的工具结果会被自动清理(Cached Micro Compact),最近 N 个结果始终保留。这引导模型在响应中记录重要信息,而非依赖工具结果的持久存在。

  7. KAIROS 自主代理模式:当 feature('KAIROS')feature('PROACTIVE') 激活时,getProactiveSection() 注入完整的自主代理行为指令——包括 tick 处理、Sleep 节奏控制、首次唤醒行为、偏向行动原则。这是一个完全不同的 system prompt 范式。

消息预处理管线

在 API 调用之前,消息经过一条完整的预处理管线。每一层都是独立的、可组合的变换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
消息预处理管线(按执行顺序)

📋 原始消息 messages[]

├─ 1. applyToolResultBudget() 裁剪大工具结果 → 持久化到磁盘,替换为预览 + 文件路径引用
│ 豁免:FileReadTool(Infinity)、最近一轮结果

├─ 2. snipCompact() 从最旧消息开始替换为 "[message snipped]" [feature: HISTORY_SNIP]
│ 保留最近 4 轮对话,< 1000 tokens 的消息不裁剪

├─ 3. microcompact() 合并冗余工具结果:FILE_UNCHANGED_STUB、空结果移除、相似结果合并
│ Cached MC 按 tool_use_id 操作,对内容替换透明

├─ 4. contextCollapse() 折叠旧的工具调用+结果对为摘要占位符 [feature: CONTEXT_COLLAPSE]
│ 保留最近 N 轮、错误结果、文件修改结果不折叠

├─ 5. autoCompact() token 使用率 > 80% 时触发 LLM 摘要压缩,目标降至 50%
│ 熔断器:压缩后至少 3 轮才能再次压缩

├─ 6. prependUserContext() 注入用户上下文(打开的文件、git 状态、选中的代码等)

├─ 7. normalizeMessages() 合并连续同角色消息、移除空消息、注入 cache_control 标记

└─▶ 📡 Claude API

设计原理:轻量级操作在前(裁剪、合并),重量级操作在后(LLM 摘要)。
如果前面的操作已将 token 数降到阈值以下,后面的操作就不会触发。

applyToolResultBudget 的详细机制:

当工具结果总大小超过预算时,按优先级裁剪最旧的大结果,将其替换为磁盘文件引用。关键细节:

  • maxResultSizeCharsInfinity 的工具(如 Read)被豁免,因为持久化 Read 结果会创建循环引用(Read→file→Read)
  • 替换记录通过 contentReplacementState 持久化,支持会话恢复时重放
  • 只有 agent:*repl_main_thread 来源的查询持久化替换记录,临时的 runForkedAgent 调用不持久化
  • 运行在 microcompact 之前——Cached MC 按 tool_use_id 操作(不检查内容),所以内容替换对它透明

管线执行顺序的设计原理:轻量级操作在前(裁剪、合并),重量级操作在后(LLM 摘要)。如果前面的操作已经将 token 数降到阈值以下,后面的操作就不会触发。Context Collapse 在 autoCompact 之前运行——如果 Collapse 已经释放了足够空间,就不需要昂贵的 LLM 摘要。

流式响应解析与工具执行

API 调用通过 queryModelWithStreaming() 发起,返回一个异步迭代器。循环逐条处理流式消息,同时进行工具执行:

backfillObservableInput 的精妙设计:

在 yield 给调用者之前,tool_use block 的 input 会被 backfillObservableInput() 处理——添加派生字段(如文件工具展开 file_path)。但这个操作在克隆的 message 上进行,原始 message 不被修改。原因是原始 message 会被 push 到 assistantMessages 并在下一次迭代中发送给 API——如果修改了原始 message,字节会不匹配,破坏 prompt cache。

withheld 错误机制

可恢复的错误(prompt-too-long、max_output_tokens、media-size-error)在流式传输中被"扣留"(withheld),不立即 yield 给调用者。它们仍然被 push 到 assistantMessages,以便后续的恢复检查可以找到它们。只有当所有恢复策略都失败时,错误才浮出。这个设计避免了 UI 闪烁——用户不会看到一个错误消息然后立即消失。

StreamingToolExecutor 并发控制

StreamingToolExecutorsrc/services/tools/StreamingToolExecutor.ts)是并行工具执行的核心。它实现了一个带并发控制的执行队列:

并发控制规则

1
2
3
4
5
6
7
private canExecuteTool(isConcurrencySafe: boolean): boolean {
const executingTools = this.tools.filter(t => t.status === 'executing')
return (
executingTools.length === 0 || // 无工具在执行
(isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe)) // 全部是并发安全的
)
}
  • 并发安全工具isConcurrencySafe=true,如 Read、Glob、Grep)可以与其他并发安全工具并行执行
  • 非并发安全工具(如 Write、Edit、Bash)必须独占执行——队列中遇到非并发安全工具时停止调度
  • isConcurrencySafe 的判断需要先 safeParse 输入——如果 parse 失败,保守地假设不安全

Bash 错误级联取消

1
2
3
4
5
if (tool.block.name === BASH_TOOL_NAME) {
this.hasErrored = true
this.erroredToolDescription = this.getToolDescription(tool)
this.siblingAbortController.abort('sibling_error')
}

Bash 工具错误会通过 siblingAbortController 级联取消所有兄弟工具。原因是 Bash 命令常有隐式依赖链(如 mkdir 失败 → 后续命令无意义)。但 Read/WebFetch 等独立工具的错误不会级联——一个文件读取失败不应该取消其他文件的读取。

中断行为分层

1
2
3
4
5
6
7
8
9
10
11
12
private getAbortReason(tool): 'sibling_error' | 'user_interrupted' | 'streaming_fallback' | null {
if (this.discarded) return 'streaming_fallback' // 模型降级,丢弃所有
if (this.hasErrored) return 'sibling_error' // Bash 错误级联
if (this.toolUseContext.abortController.signal.aborted) {
if (signal.reason === 'interrupt') { // 用户提交新消息
return this.getToolInterruptBehavior(tool) === 'cancel'
? 'user_interrupted' : null // 'block' 工具继续运行
}
return 'user_interrupted' // 硬取消(Escape)
}
return null
}

interruptBehavior'cancel' 的工具(如 Read)在用户中断时被取消,'block' 的工具(如 Write)继续运行直到完成。这防止了文件写入被中途打断导致数据损坏。

Progress 消息的即时传递

工具执行过程中的 progress 类型消息(如 Bash 命令的实时输出)被存储在 pendingProgress 数组中,通过 progressAvailableResolve 信号唤醒 getRemainingResults() 的等待循环。这保证了用户可以实时看到长时间运行的命令输出,而不需要等待命令完成。

结果顺序保证

getCompletedResults() 按工具接收顺序遍历——如果一个非并发安全工具还在执行,即使后面的并发安全工具已完成,也不会 yield 后面的结果。这保证了消息顺序与模型的 tool_use 顺序一致。

错误恢复机制

循环内置了多层错误恢复,形成一个优先级递减的恢复链:

prompt-too-long 恢复的三级策略

  1. Context Collapse drain(最优先):释放已暂存的上下文折叠。如果上一次迭代的 transition.reason 已经是 'collapse_drain_retry'(说明 drain 后重试仍然 413),则跳过直接进入下一级。
  2. Reactive Compact(次优先):完整的 LLM 压缩摘要。hasAttemptedReactiveCompact 标志防止无限重试。
  3. 错误浮出:两者都失败时,yield 扣留的错误消息,执行 StopFailure Hooks,返回 Terminal: prompt_too_long

关键防护:恢复失败时不执行 Stop Hooks——模型从未产生有效响应,Hooks 没有有意义的内容可评估。运行 Stop Hooks 会创建死亡螺旋:error → hook blocking → retry → error → …(Hook 每次注入更多 token)。

max_output_tokens 的两阶段恢复

  1. 升级重试tengu_otk_slot_v1 特性标志):如果使用了默认的 8K 上限且被截断,直接以 64K(ESCALATED_MAX_TOKENS)重试同一请求——无 meta 消息、无多轮对话。这是一次性的(由 maxOutputTokensOverride === undefined 守卫)。
  2. 多轮恢复:如果升级后仍被截断,注入 "Output token limit hit. Resume directly—no apology, no recap. Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces." 消息,最多 3 次。

模型降级的完整清理

降级时需要清理所有与旧模型相关的状态:

  • 清空 assistantMessagestoolResultstoolUseBlocks
  • 丢弃旧的 StreamingToolExecutor,创建新的
  • 为孤立的 assistant messages yield tombstone 事件(UI 移除这些消息)
  • Ant 用户额外执行 stripSignatureBlocks()——thinking 签名是模型绑定的,将 Capybara 的签名发送给 Opus 会导致 400 错误

Token Budget 与 Task Budget

Claude Code 支持两种不同层面的 token 预算机制:

维度 Token Budget Task Budget
层面 客户端级别 API 级别
触发方式 用户指定(如 "+500k" output_config.task_budget
实现 createBudgetTracker() task-budgets-2026-03-13 beta
检查时机 每次迭代结束 API 请求参数
跨压缩 重置(新的追踪周期) taskBudgetRemaining 递减
自动继续 注入 nudge 消息 服务端控制

Token Budget 的 diminishing returns 检测:当连续多次继续但输出 token 增长率下降时,提前停止——避免模型陷入重复输出的死循环。

Task Budget 的跨压缩追踪taskBudgetRemaining 在循环外声明(不在 State 上),避免触碰 7 个 continue 站点。未压缩时为 undefined(服务端可以看到完整历史自行计算),压缩后递减(服务端只看到摘要,无法计算已消耗量)。

轮次间的附件注入

每次工具执行完成后、进入下一次迭代前,循环注入多种附件消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 1. 排队命令快照(slash commands、task notifications)
const queuedCommandsSnapshot = getCommandsByMaxPriority(sleepRan ? 'later' : 'next')
.filter(cmd => {
if (isSlashCommand(cmd)) return false // Slash 命令走 processSlashCommand
if (isMainThread) return cmd.agentId === undefined // 主线程只消费无 agentId 的命令
return cmd.mode === 'task-notification' && cmd.agentId === currentAgentId // 子代理只消费自己的
})

// 2. 通用附件(文件变更检测、编辑预览等)
for await (const attachment of getAttachmentMessages(...)) {
yield attachment
}

// 3. Memory Prefetch 消费(零等待,未就绪则跳过)
if (pendingMemoryPrefetch?.settledAt !== null && consumedOnIteration === -1) {
const memoryAttachments = filterDuplicateMemoryAttachments(
await pendingMemoryPrefetch.promise,
toolUseContext.readFileState // 过滤模型已 Read/Write/Edit 的文件
)
}

// 4. Skill Discovery Prefetch 消费
if (pendingSkillPrefetch) {
const skillAttachments = await skillPrefetch.collectSkillDiscoveryPrefetch(pendingSkillPrefetch)
}

// 5. 刷新工具列表(新连接的 MCP 服务器)
if (updatedToolUseContext.options.refreshTools) {
const refreshedTools = updatedToolUseContext.options.refreshTools()
}

命令队列的代理隔离:队列是进程全局单例,被协调器和所有进程内子代理共享。每个循环只消费属于自己的命令——主线程消费 agentId === undefined 的命令,子代理消费 agentId === currentAgentId 的命令。用户 prompt(mode: 'prompt')始终只发给主线程。

Sleep 工具的队列刷新:当 SleepTool 在本轮执行过(sleepRan = true),命令队列的优先级阈值从 'next' 提升到 'later'——这允许 KAIROS 自主代理在 Sleep 后消费低优先级的任务通知。

Feature Gate 系统

源码大量使用 feature('FLAG_NAME') 进行编译时门控。这是 Bun 的 bun:bundle 提供的编译时常量折叠机制——在 npm 发布的外部构建中,所有 feature() 调用被替换为 false,相关代码被死代码消除(DCE)。这意味着 108 个内部模块在外部构建中完全不存在:

1
2
3
4
5
6
7
8
// 编译时门控示例——feature() 只能出现在 if/ternary 条件中
const reactiveCompact = feature('REACTIVE_COMPACT')
? require('./services/compact/reactiveCompact.js')
: null

const contextCollapse = feature('CONTEXT_COLLAPSE')
? require('./services/contextCollapse/index.js')
: null

DCE 约束feature() 调用必须出现在 if 或三元表达式的条件位置——不能赋值给变量后再判断,否则 Bun 的 tree-shaking 无法识别。同样,process.env.USER_TYPE === 'ant' 必须在每个调用点内联,不能提升为 const。

已识别的 Feature Flags 按功能分类:

类别 Flags
上下文管理 REACTIVE_COMPACT, CONTEXT_COLLAPSE, HISTORY_SNIP, CACHED_MICROCOMPACT, TOKEN_BUDGET
多代理 FORK_SUBAGENT, COORDINATOR_MODE, BG_SESSIONS, VERIFICATION_AGENT
自主代理 (KAIROS) KAIROS, KAIROS_BRIEF, KAIROS_CHANNELS, KAIROS_PUSH_NOTIFICATION, KAIROS_GITHUB_WEBHOOKS, PROACTIVE
工具 MONITOR_TOOL, WEB_BROWSER_TOOL, OVERFLOW_TEST_TOOL, BASH_CLASSIFIER, TRANSCRIPT_CLASSIFIER
扩展 EXPERIMENTAL_SKILL_SEARCH, MCP_SKILLS, WORKFLOW_SCRIPTS, AGENT_TRIGGERS, AGENT_TRIGGERS_REMOTE, TEMPLATES
基础设施 BRIDGE_MODE, CHICAGO_MCP, TERMINAL_PANEL, UDS_INBOX, PROMPT_CACHE_BREAK_DETECTION, TEAMMEM

三、工具系统(Tool System)

Claude Code 内置 40+ 工具,通过统一的 Tool 接口和 buildTool() 工厂函数构建。工具系统的设计核心是fail-closed 安全默认值并发感知的执行模型

工具接口与 buildTool 工厂

所有工具都通过 src/Tool.ts 中的 Tool 接口定义,并通过 buildTool() 工厂函数构建。Tool 接口定义了 40+ 个方法和属性,核心包括:

方法/属性 类型 说明
name string 工具名称,API 中的 tool name
aliases string[] 向后兼容别名(旧名称仍可匹配权限规则)
inputSchema z.ZodType Zod schema,定义输入参数
call() async generator 工具执行逻辑,yield 中间结果和最终结果
description() async 动态生成工具描述(给用户看,显示在 UI 中)
prompt() async 生成 AI-facing prompt(给模型看,注入 system prompt)
checkPermissions() async 工具级权限检查(返回 allow/deny/ask + updatedInput)
validateInput() async 输入验证(在权限检查之前执行)
isConcurrencySafe() (input) => boolean 是否可并行执行(接收 parsed input)
isReadOnly() boolean 是否只读(影响权限默认值)
isDestructive() boolean 是否不可逆(影响 UI 警告级别)
interruptBehavior() () => 'cancel'|'block' 用户中断时的行为
maxResultSizeChars number 结果超此大小则持久化到磁盘(Infinity 表示豁免)
shouldDefer boolean 是否延迟加载(ToolSearch 模式下不在初始 schema 中)
alwaysLoad boolean 是否始终加载(不受 ToolSearch 影响)
searchHint string ToolSearch 关键词匹配提示(3-10 词)
strict boolean 严格模式(API 层面更严格遵守 schema)
backfillObservableInput() (input) => void 回填可观察输入(不修改原始输入,保护 prompt cache)
preparePermissionMatcher() async 准备权限匹配器(如 Bash(git *) 模式匹配)
toAutoClassifierInput() (input) => string 生成分类器输入(Auto Mode 安全判断)
isSearchOrReadCommand() (input) => {...} 判断是否为搜索/读取操作(UI 折叠显示)
getActivityDescription() (input) => string 生成活动描述(Spinner 显示)
renderToolUseMessage() React 组件 渲染工具调用消息
renderToolResultMessage() React 组件 渲染工具结果消息
renderGroupedToolUse() React 组件 渲染并行工具组

buildTool() 提供安全的默认值(fail-closed 原则):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: (_input?) => false, // 假设不安全——新工具默认独占执行
isReadOnly: (_input?) => false, // 假设会写入——新工具默认需要权限
isDestructive: (_input?) => false,
checkPermissions: (input, _ctx?) => // 延迟到通用权限系统
Promise.resolve({ behavior: 'allow', updatedInput: input }),
toAutoClassifierInput: (_input?) => '', // 空字符串 → 跳过分类器
userFacingName: (_input?) => '',
}

export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
return {
...TOOL_DEFAULTS,
userFacingName: () => def.name,
...def,
} as BuiltTool<D>
}

类型级别的精确性BuiltTool<D> 是一个条件映射类型——对于每个 defaultable key,如果 D 提供了该方法(required),使用 D 的类型;如果 D 省略了(inherited from Partial<>),使用默认值的类型。这保证了 60+ 个工具的零类型错误。

工具生命周期

每次工具调用经过以下生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
工具调用生命周期

├── 1. inputSchema.safeParse(input) // Zod schema 验证
│ └── 失败 → 返回错误给模型

├── 2. validateInput(input, context) // 工具级输入验证
│ └── 失败 → 返回验证错误给模型

├── 3. PreToolUse Hooks // 用户定义的前置 Hook
│ ├── Command Hook(shell 命令)
│ ├── Prompt Hook(LLM 判断)
│ └── HTTP Hook(远程端点)
│ └── 结果:allow / deny / ask / updatedInput

├── 4. hasPermissionsToUseTool() // 规则匹配引擎
│ ├── deny 规则 → 直接拒绝
│ ├── ask 规则 → 进入交互式确认
│ ├── allow 规则 → 检查 Auto Mode 分类器
│ └── 无匹配 → checkPermissions()

├── 5. checkPermissions(input, context) // 工具级权限检查
│ └── Bash: 命令解析 + 路径验证 + 沙箱检查
│ └── File: 路径权限 + 工作目录验证

├── 6. [交互式确认] // 如果需要用户确认
│ ├── Terminal UI / Bridge UI / Channel UI
│ └── Allow Once / Allow Always / Deny

├── 7. call(input, context, ...) // 工具执行
│ ├── yield progress 消息 // 实时进度
│ └── return ToolResult<Output> // 最终结果

├── 8. PostToolUse Hooks // 后置 Hook
│ └── 可注入 systemMessage

└── 9. mapToolResultToToolResultBlockParam() // 格式化为 API 响应

工具注册与过滤

src/tools.ts 中的 getAllBaseTools() 是所有工具的注册中心。工具列表的组装考虑了多个条件:

Prompt Cache 稳定性是工具排序的核心考量——内置工具必须作为连续前缀,MCP 工具排在后面。如果混合排序,MCP 工具的增减会导致内置工具的位置变化,破坏所有下游的 cache key。uniqBy('name') 保证内置工具在名称冲突时优先——如果 MCP 服务器提供了与内置工具同名的工具,内置版本胜出。

完整工具清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
FILE OPERATIONS          SEARCH & DISCOVERY        EXECUTION
═════════════════ ══════════════════════ ══════════
FileReadTool GlobTool BashTool
FileEditTool GrepTool PowerShellTool
FileWriteTool ToolSearchTool
NotebookEditTool INTERACTION
═══════════
WEB & NETWORK AGENT / TASK AskUserQuestionTool
════════════════ ══════════════════ BriefTool
WebFetchTool AgentTool
WebSearchTool SendMessageTool PLANNING & WORKFLOW
TeamCreateTool ════════════════════
MCP PROTOCOL TeamDeleteTool EnterPlanModeTool
══════════════ TaskCreateTool ExitPlanModeTool
MCPTool TaskGetTool EnterWorktreeTool
ListMcpResourcesTool TaskUpdateTool ExitWorktreeTool
ReadMcpResourceTool TaskListTool TodoWriteTool
TaskStopTool
TaskOutputTool SYSTEM
════════
SKILLS & EXTENSIONS ConfigTool
═════════════════════ SkillTool
SkillTool ScheduleCronTool
LSPTool SleepTool
TungstenTool

BashTool 深度分析

BashTool(src/tools/BashTool/BashTool.tsx,157.88KB)是最复杂的工具,包含完整的命令安全分析系统。

命令执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
BashTool.call(input)

├── 1. 命令解析
│ ├── parseCommand() → AST
│ ├── 提取命令名、参数、管道、重定向
│ └── 识别 shell 特殊语法(&&, ||, ;, |)

├── 2. 安全分析(bashSecurity.ts, 102.69KB)
│ ├── classifyCommand() → 命令语义分类
│ │ ├── read-only(ls, cat, grep, find, git log)
│ │ ├── write(mkdir, cp, mv, touch)
│ │ ├── destructive(rm, git push --force)
│ │ └── network(curl, wget, ssh)
│ ├── validatePaths() → 路径沙箱验证
│ │ ├── 检查是否在工作目录内
│ │ ├── 检查 additionalWorkingDirectories
│ │ └── 拒绝 /etc, /usr, ~ 等敏感路径
│ ├── validateSedCommand() → sed 命令解析
│ │ ├── 提取 sed 表达式
│ │ ├── 验证目标文件路径
│ │ └── 检查是否为 in-place 编辑(-i)
│ └── checkDestructivePatterns() → 破坏性模式检测
│ ├── rm -rf /
│ ├── git push --force
│ ├── chmod 777
│ └── > /dev/sda

├── 3. 沙箱执行
│ ├── macOS: seatbelt sandbox-exec
│ │ └── 限制文件系统、网络、进程访问
│ ├── Linux: 容器隔离
│ └── Windows: 无沙箱(PowerShellTool 替代)

├── 4. 进程管理
│ ├── spawn 子进程
│ ├── 实时 stdout/stderr 流式传输(yield progress)
│ ├── 超时控制(默认 120s,可配置)
│ └── 信号处理(SIGTERM → SIGKILL)

└── 5. 结果处理
├── exit code 检查
├── 输出截断(超过 maxResultSizeChars)
└── 错误级联(通知 StreamingToolExecutor)

Bash 错误级联:Bash 工具错误会通过 siblingAbortController 级联取消所有兄弟工具。原因是 Bash 命令常有隐式依赖链(如 mkdir 失败 → 后续命令无意义)。但 Read/WebFetch 等独立工具的错误不会级联。

FileEditTool 的 string-replace 策略

FileEditTool(src/tools/FileEditTool/FileEditTool.ts)使用 string-replace 编辑策略而非 diff/patch:

1
2
3
4
5
6
7
// 输入 schema
{
file_path: string, // 目标文件路径
old_string: string, // 要替换的精确文本
new_string: string, // 替换后的文本
replace_all?: boolean, // 是否替换所有匹配(默认 false)
}

设计理由

  1. 精确匹配old_string 必须精确匹配文件中的文本(包括空白),避免 diff 的模糊匹配导致的错误编辑
  2. 原子性:如果 old_string 不匹配或匹配多处(且 replace_all=false),操作失败——不会产生部分编辑
  3. Prompt Cache 友好:模型只需要生成变更的部分,不需要重复整个文件内容
  4. 可审计:用户可以清楚看到"什么被替换为什么"

ToolSearch 延迟加载

当工具数量过多时(特别是 MCP 工具),所有工具的 schema 会占用大量上下文窗口。ToolSearch 机制解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ToolSearch 工作流程

├── 启动时
│ ├── 标记 shouldDefer=true 的工具为延迟加载
│ ├── 标记 alwaysLoad=true 的工具为始终加载
│ ├── 计算所有 MCP 工具描述的总大小
│ └── 如果总大小 > 上下文窗口的 10% → 自动启用 ToolSearch

├── 初始 API 请求
│ ├── 只包含非延迟工具的完整 schema
│ ├── 延迟工具以 defer_loading: true 发送(只有名称和 searchHint)
│ └── ToolSearchTool 本身始终加载

├── 模型需要延迟工具时
│ ├── 调用 ToolSearchTool(query="...")
│ ├── 关键词匹配 searchHint + 工具名称
│ └── 返回匹配工具的完整 schema

└── 后续请求
└── 已搜索到的工具 schema 持久化在上下文中

MCP 工具的 alwaysLoad:MCP 工具可以通过 _meta['anthropic/alwaysLoad'] 标记为始终加载——用于模型在第一轮就必须看到的关键工具,无需 ToolSearch 往返。

REPL 模式

isReplModeEnabled() 为 true 时(Ant-only),REPL_ONLY_TOOLS(包括 Bash、Read、Edit、Glob、Grep 等原始工具)被隐藏,所有操作通过 REPLTool 的 VM 上下文执行。这是一种更高级的工具封装模式——模型不再直接调用文件系统工具,而是在一个持久化的 REPL 环境中执行代码。

工具结果持久化

当工具结果超过 maxResultSizeChars 时,结果被持久化到磁盘,模型收到一个预览 + 文件路径引用:

1
2
3
4
5
6
// maxResultSizeChars 的典型值
MCPTool: 100_000 // 100K 字符
SkillTool: 100_000
LSPTool: 100_000
GrepTool: 30_000 // 搜索结果较小
FileReadTool: Infinity // 豁免——持久化会创建循环引用

FileReadToolmaxResultSizeChars = Infinity 是关键设计——如果 Read 结果被持久化到文件,模型需要再次 Read 该文件来获取内容,形成 Read→file→Read 的循环引用。Read 工具通过自身的 fileReadingLimitsmaxTokensmaxSizeBytes)自行限制输出大小。

工具 Prompt 的动态生成

每个工具的 prompt() 方法生成 AI-facing 的使用指南,注入到 system prompt 中。Prompt 内容是动态的——根据当前环境、权限模式、可用工具等条件变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// BashTool.prompt() 的条件分支示例
async prompt({ getToolPermissionContext, tools }) {
const ctx = await getToolPermissionContext()
let prompt = BASE_BASH_PROMPT

if (ctx.mode === 'bypassPermissions') {
prompt += YOLO_MODE_ADDENDUM // "你可以自由执行任何命令"
}
if (isSandboxingEnabled()) {
prompt += SANDBOX_ADDENDUM // "命令在沙箱中执行,某些操作受限"
}
if (hasGrepTool(tools)) {
prompt += "对于搜索,优先使用 GrepTool 而非 bash grep"
}
return prompt
}

中断行为分层

1
2
3
// interruptBehavior 的语义
'cancel' // 用户中断时立即取消(Read、Grep、Glob 等只读工具)
'block' // 用户中断时继续运行直到完成(Write、Edit 等写入工具)

'block' 行为防止文件写入被中途打断导致数据损坏。当用户按 Escape 时,'cancel' 工具被取消,'block' 工具继续运行。当用户提交新消息时(软中断),只有 interruptBehavior === 'cancel' 的工具被取消。


四、权限与安全系统

Claude Code 实现了一个多层纵深防御的权限模型,从 Hook 前置检查到规则匹配到交互式确认到工具级沙箱,确保每次工具调用都经过严格的安全审查。

四层权限检查流程

src/hooks/useCanUseTool.tsx 中的 useCanUseTool Hook 是权限检查的入口。完整的权限检查流程:

规则匹配引擎

src/utils/permissions/permissions.ts 中的 hasPermissionsToUseTool() 实现了规则匹配。规则来源按优先级排列(高 → 低):

1
2
3
4
5
6
7
8
9
10
规则优先级(高 → 低)

├── managed-settings // 企业管理员设置(最高优先级)
├── user // 用户全局设置(~/.claude/settings.json)
├── userLocal // 用户本地设置(~/.claude/settings.local.json)
├── project // 项目设置(.claude/settings.json)
├── projectLocal // 项目本地设置(.claude/settings.local.json)
├── cliArg // CLI 参数(--allowedTools, --disallowedTools)
├── command // 斜杠命令设置
└── session // 会话内用户决策(Allow Always / Deny)

规则值支持多种模式

模式 示例 说明
精确匹配 BashReadWrite 匹配工具名称
带内容匹配 Bash(git:*) 匹配工具名称 + 命令前缀
通配符 Bash(npm *) 通过 preparePermissionMatcher() 预编译
MCP 工具 mcp__github__create_issue 匹配特定 MCP 工具
MCP 服务器前缀 mcp__github 匹配该服务器所有工具

deny 规则的绝对优先级deny 规则具有最高优先级,任何其他规则都无法覆盖。managed 来源的 ask 规则不能被用户的 allow 规则覆盖——这保证了企业管理员可以强制要求某些工具必须经过审批。

hasPermissionsToUseToolInner 的完整逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
async function hasPermissionsToUseToolInner(tool, input, context) {
// 1a. 检查 deny 规则(整个工具被拒绝)
const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool)
if (denyRule) return { behavior: 'deny', ... }

// 1b. 检查 ask 规则(整个工具需要确认)
const askRule = getAskRuleForTool(appState.toolPermissionContext, tool)
if (askRule) {
// 特殊情况:沙箱模式下 Bash 可以自动放行
const canSandboxAutoAllow =
tool.name === BASH_TOOL_NAME &&
SandboxManager.isSandboxingEnabled() &&
SandboxManager.isAutoAllowBashIfSandboxedEnabled() &&
shouldUseSandbox(input)
if (!canSandboxAutoAllow) return { behavior: 'ask', ... }
// 否则 fall through 到 checkPermissions
}

// 1c. 工具级权限检查(如 Bash 子命令规则)
const toolPermissionResult = await tool.checkPermissions(parsedInput, context)
// toolPermissionResult.behavior: 'allow' | 'deny' | 'ask' | 'passthrough'

// 2. 检查 allow 规则
const allowRule = getAllowRuleForTool(appState.toolPermissionContext, tool, input)
if (allowRule) return { behavior: 'allow', ... }

// 3. 如果工具说 allow 且没有 ask 规则 → 放行
if (toolPermissionResult.behavior === 'allow') return { behavior: 'allow', ... }

// 4. 默认 → ask
return { behavior: 'ask', ... }
}

Auto Mode 分类器

当权限模式为 auto 时,classifyYoloAction() 使用 LLM 分类器判断工具调用是否安全:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Auto Mode 分类器流程

├── 输入构建
│ ├── tool.toAutoClassifierInput(input) // 工具特定的分类器输入
│ │ ├── Bash: 完整命令字符串
│ │ ├── Edit: "path: content"
│ │ └── Write: "path: content"
│ └── 对话上下文(最近的消息摘要)

├── 分类器类型
│ ├── Bash Classifier(feature: BASH_CLASSIFIER)
│ │ └── 专门分析 Bash 命令的安全性
│ └── Transcript Classifier(feature: TRANSCRIPT_CLASSIFIER)
│ └── 分析完整对话上下文

├── 分类结果
│ ├── { matches: true, confidence: 'high' } → 自动放行
│ ├── { matches: true, confidence: 'low' } → 交互式确认
│ └── { matches: false } → 交互式确认

└── Denial Tracking 防死锁
├── recordDenial() 记录每次拒绝
├── 连续拒绝达到 DENIAL_LIMITS 阈值
└── shouldFallbackToPrompting() → 降级为交互式确认

Speculative Classifier:对于 Bash 工具,在显示权限对话框之前,先启动一个 2 秒超时的推测性分类。如果分类器在 2 秒内返回 high-confidence match,直接放行,用户不会看到权限提示。这显著改善了 Auto Mode 的用户体验——大多数安全命令(lscatgit status)可以在用户无感知的情况下通过。

Hook 系统

src/utils/hooks.ts(160.62KB,5023 行)实现了完整的 Hook 执行引擎。

Hook 事件类型

事件 触发时机 可控制
PreToolUse 工具执行前 approve/deny/modify input
PostToolUse 工具执行后 inject systemMessage
PostToolUseFailure 工具执行失败后 通知
PermissionRequest 权限请求时 allow/deny
PermissionDenied 权限被拒绝后 通知
Notification 通用通知 通知
Stop 循环即将停止 blocking/preventContinuation
SubagentStop 子代理即将停止 blocking/preventContinuation
PreCompact 压缩前 customInstructions
PostCompact 压缩后 systemMessage
SessionStart 会话开始 systemMessage
SessionEnd 会话结束 通知
InstructionsLoaded CLAUDE.md 加载后 通知
FileChanged 文件变更后 通知
TaskCreated 任务创建后 通知
Elicitation MCP 请求输入 自动回答

三种 Hook 类型

类型 实现 通信方式 超时 适用场景
Command spawn 子进程 stdin JSON → stdout JSON/text 10 分钟 本地脚本、CI 集成
Prompt execPromptHook() 模板化 prompt → LLM 响应 取决于 LLM 复杂判断、上下文感知
HTTP execHttpHook() POST JSON → JSON 响应 10 分钟 远程服务、Webhook

Command Hook 的输出解析

1
2
3
4
5
6
7
8
9
function parseHookOutput(stdout: string) {
const trimmed = stdout.trim()
if (!trimmed.startsWith('{')) {
return { plainText: stdout } // 纯文本模式——内容作为 systemMessage 注入
}
// JSON 模式——验证 Zod schema
// 支持字段:continue, suppressOutput, stopReason, decision,
// systemMessage, permissionDecision, hookSpecificOutput
}

异步 HookregisterPendingAsyncHook() 支持后台执行的异步 Hook。asyncRewake 模式下,Hook 完成后如果 exit code 为 2(blocking error),通过 enqueuePendingNotification() 唤醒模型。

Hook 来源控制

  • shouldDisableAllHooksIncludingManaged():禁用所有 Hook(调试模式)
  • shouldAllowManagedHooksOnly():只允许 managed Hook(企业管理)
  • isSourceAdminTrusted():判断 Hook 来源是否为 admin-trusted

resolveHookPermissionDecision 的不变量

src/services/tools/toolHooks.ts 中的 resolveHookPermissionDecision() 封装了一个关键不变量:Hook 的 allow 不能绕过 settings.json 的 deny/ask 规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Hook allow 的处理逻辑
if (hookPermissionResult?.behavior === 'allow') {
const hookInput = hookPermissionResult.updatedInput ?? input

// 如果工具需要用户交互且 Hook 提供了 updatedInput → 视为交互已满足
const interactionSatisfied =
requiresInteraction && hookPermissionResult.updatedInput !== undefined

// 即使 Hook approve,deny/ask 规则仍然生效
const ruleCheck = await checkRuleBasedPermissions(tool, hookInput, toolUseContext)
if (ruleCheck === null) {
// 无规则阻止 → Hook 的 allow 生效
return { decision: hookPermissionResult, input: hookInput }
}
if (ruleCheck.behavior === 'deny') {
// deny 规则覆盖 Hook 的 allow
return { decision: ruleCheck, input: hookInput }
}
// ask 规则 → 仍需交互式确认
}

这个设计防止了安全漏洞:恶意 Hook 不能通过返回 allow 来绕过管理员设置的 deny 规则。

Bash 安全模型

src/tools/BashTool/bashSecurity.ts(102.69KB)实现了 Bash 命令的深度安全分析:

命令语义分类

分类 示例 权限
只读 ls, cat, grep, find, git log, wc 自动放行
写入 mkdir, cp, mv, touch, git add 需要确认
破坏性 rm -rf, git push --force, chmod 777 强制确认 + 警告
网络 curl, wget, ssh, nc 需要确认
包管理 npm install, pip install, apt-get 需要确认

路径验证pathValidation.ts,43.93KB):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
路径验证规则

├── 工作目录检查
│ ├── 命令中的所有路径必须在 CWD 或 additionalWorkingDirectories 内
│ ├── 符号链接解析后重新检查
│ └── 相对路径解析为绝对路径后检查

├── 敏感路径拒绝
│ ├── /etc, /usr, /bin, /sbin
│ ├── ~/.ssh, ~/.gnupg, ~/.aws
│ ├── /dev, /proc, /sys
│ └── Windows: C:\Windows, C:\Program Files

├── sed -i 特殊处理
│ ├── 解析 sed 表达式
│ ├── 提取目标文件路径
│ ├── 验证文件路径在工作目录内
│ └── 拒绝对系统文件的 in-place 编辑

└── 只读验证(readOnlyValidation.ts, 68.66KB)
├── Plan Mode 下所有写入操作被拒绝
├── 只读工作目录中的写入被拒绝
└── 只读 MCP 工具的写入被拒绝

沙箱模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
沙箱执行模型

├── macOS: sandbox-exec (seatbelt)
│ ├── 限制文件系统访问(只允许工作目录)
│ ├── 限制网络访问(可配置)
│ ├── 限制进程创建
│ └── 排除命令列表(某些命令不适合沙箱)

├── Linux: 容器隔离
│ ├── namespace 隔离
│ ├── cgroup 资源限制
│ └── seccomp 系统调用过滤

├── autoAllowBashIfSandboxed
│ ├── 沙箱启用时,Bash 命令自动放行
│ ├── 不适合沙箱的命令(排除列表)仍需确认
│ └── dangerouslyDisableSandbox 时不生效

└── Windows: 无沙箱
└── PowerShellTool 作为替代

权限模式

模式 说明 行为
default 默认模式 按规则匹配,未匹配则交互式确认
auto 自动模式 LLM 分类器判断,高置信度自动放行
bypassPermissions 绕过模式 所有工具自动放行(危险)
plan 计划模式 只允许只读工具,写入工具被拒绝

Plan Mode 的进入/退出

  • 进入:EnterPlanModeTool 或模型自主决定
  • 退出:ExitPlanModeTool
  • 保存:prePlanMode 字段记录进入前的模式,退出时恢复

Workspace Trust

所有 Hook 执行都需要 workspace trust——因为 Hook 执行来自 .claude/settings.json 的任意命令。

历史安全漏洞

  • SessionEnd hooks 在用户拒绝 trust dialog 时执行
  • SubagentStop hooks 在子代理完成前执行
  • 这些都已修复,现在所有 Hook 都严格检查 trust 状态

ToolPermissionContext 的不可变性

ToolPermissionContext 使用 DeepImmutable<> 类型包装,确保权限上下文在传递过程中不被意外修改:

1
2
3
4
5
6
7
8
9
10
11
12
export type ToolPermissionContext = DeepImmutable<{
mode: PermissionMode
additionalWorkingDirectories: Map<string, AdditionalWorkingDirectory>
alwaysAllowRules: ToolPermissionRulesBySource
alwaysDenyRules: ToolPermissionRulesBySource
alwaysAskRules: ToolPermissionRulesBySource
isBypassPermissionsModeAvailable: boolean
isAutoModeAvailable?: boolean
shouldAvoidPermissionPrompts?: boolean // 后台代理自动拒绝
awaitAutomatedChecksBeforeDialog?: boolean // 协调器等待自动化检查
prePlanMode?: PermissionMode // Plan Mode 前的模式
}>

权限更新通过 setAppState 的函数式更新实现——创建新的 context 对象,而非修改现有对象。这保证了并发安全——多个工具可能同时检查权限。


五、上下文管理与压缩(Compaction)

Claude Code 的上下文管理系统解决了 LLM 的核心限制——有限的上下文窗口。系统通过五层渐进式管线,在保留关键信息的同时最大化可用上下文空间。

五层管线总览

每次 API 调用前,消息列表经过以下五层管线处理(按执行顺序):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
原始消息 messages[]

├── Layer 1: applyToolResultBudget() // 大工具结果裁剪
│ ├── 计算所有工具结果的总 token 数
│ ├── 超过 toolResultBudget 的结果 → 持久化到磁盘
│ └── 替换为预览 + 文件路径引用

├── Layer 2: snipCompact() // 历史裁剪 [feature: HISTORY_SNIP]
│ ├── 从最旧的消息开始
│ ├── 替换为 "[message snipped]" 占位符
│ └── 保留最近 N 条消息不裁剪

├── Layer 3: microcompact() // 微压缩
│ ├── 合并连续的 FILE_UNCHANGED 结果
│ ├── 折叠重复的工具调用模式
│ └── 替换为 FILE_UNCHANGED_STUB

├── Layer 4: contextCollapse() // 上下文折叠 [feature: CONTEXT_COLLAPSE]
│ ├── 识别可折叠的上下文块
│ ├── 替换为摘要占位符
│ └── 保留关键的工具结果不折叠

└── Layer 5: autoCompact() // 自动压缩
├── 检查 token 使用率是否超过阈值
├── 超过 → 调用 LLM 生成摘要
└── 替换历史消息为压缩摘要

Layer 1: applyToolResultBudget

src/utils/messages/applyToolResultBudget.ts 实现了工具结果的预算裁剪。

触发条件:当所有工具结果的总 token 数超过 toolResultBudget 时触发。

裁剪策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
applyToolResultBudget 裁剪策略

├── 1. 计算每个工具结果的 token 数
│ └── 使用 countTokens() 精确计算

├── 2. 按 token 数降序排列

├── 3. 从最大的结果开始裁剪
│ ├── 将完整结果写入磁盘文件
│ ├── 替换为预览(前 N 行 + 后 N 行)
│ └── 附加文件路径引用

├── 4. 重复直到总 token 数 < toolResultBudget

└── 5. 特殊豁免
├── FileReadTool 结果(maxResultSizeChars = Infinity)
├── 最近一轮的工具结果(保留完整性)
└── 已经被裁剪过的结果(避免重复裁剪)

Layer 2: snipCompact

src/utils/messages/snipCompact.ts 实现了基于历史位置的裁剪。

核心逻辑:从最旧的消息开始,将消息内容替换为 [message snipped] 占位符。保留最近的消息不裁剪——这些消息包含当前任务的关键上下文。

1
2
3
// snipCompact 的关键参数
const SNIP_PRESERVE_RECENT = 4 // 保留最近 4 轮对话
const SNIP_MIN_TOKENS = 1000 // 消息 < 1000 tokens 不裁剪(开销不值得)

Feature GateHISTORY_SNIP 门控。未启用时跳过此层。

Layer 3: microcompact

src/utils/messages/microcompact.ts 实现了细粒度的冗余消除。

三种微压缩策略

策略 触发条件 替换内容
FILE_UNCHANGED 连续多次 Read 同一文件且内容未变 FILE_UNCHANGED_STUB
TOOL_RESULT_MERGE 连续相同工具的相似结果 合并为单条
EMPTY_RESULT_STRIP 空的工具结果 移除
1
2
3
4
// FILE_UNCHANGED_STUB 的格式
const FILE_UNCHANGED_STUB = '<file_unchanged />'
// 当模型连续 Read 同一文件时,第二次及之后的结果被替换为此 stub
// 节省大量 token(一个大文件可能 10K+ tokens)

Layer 4: contextCollapse

src/utils/messages/contextCollapse.ts 实现了上下文块的折叠。

折叠策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
contextCollapse 折叠策略

├── 识别可折叠块
│ ├── 连续的工具调用 + 结果对
│ ├── 长的 assistant 消息
│ └── 旧的 user 消息

├── 折叠规则
│ ├── 保留最近 N 轮不折叠
│ ├── 保留包含错误的工具结果
│ ├── 保留包含文件修改的工具结果
│ └── 保留用户明确引用的内容

├── 折叠格式
│ ├── 工具调用 → "[Tool: {name}({summary})]"
│ ├── 工具结果 → "[Result: {brief_summary}]"
│ └── 长消息 → "[Message: {first_line}... (collapsed)]"

└── drain 机制
├── prompt-too-long 错误时触发
├── 逐步展开已折叠的块
└── 为 reactive compact 腾出空间

Feature GateCONTEXT_COLLAPSE 门控。

drain 机制是 contextCollapse 的关键特性——当 API 返回 prompt-too-long(413)错误时,系统不是立即触发昂贵的 LLM 压缩,而是先尝试"排水"已折叠的上下文块。这是一个零成本的恢复策略。

Layer 5: autoCompact

src/utils/autoCompact.ts 是最重量级的压缩层——调用 LLM 生成对话摘要。

触发条件

1
2
3
4
5
6
7
8
9
10
11
12
// autoCompact 触发阈值
const AUTO_COMPACT_THRESHOLD = 0.8 // token 使用率 > 80%
const AUTO_COMPACT_TARGET = 0.5 // 压缩后目标使用率 50%

// 计算公式
const currentUsage = countTokens(messages) / contextWindowSize
if (currentUsage > AUTO_COMPACT_THRESHOLD) {
await compactConversation(messages, {
targetUsage: AUTO_COMPACT_TARGET,
...options
})
}

熔断器

1
2
3
4
5
6
7
8
// 防止压缩循环的熔断器
const COMPACT_COOLDOWN = 3 // 压缩后至少 3 轮对话才能再次压缩
let turnsSinceLastCompact = 0

if (turnsSinceLastCompact < COMPACT_COOLDOWN) {
// 跳过 autoCompact,即使超过阈值
// 防止"压缩 → 新消息 → 又超阈值 → 再压缩"的死循环
}

compactConversation 完整流程

src/utils/compactConversation.ts 实现了 LLM 驱动的对话压缩:

压缩 Prompt 的关键指令

1
2
3
4
5
6
7
8
9
10
11
12
你正在压缩一段对话历史。请生成一个简洁的摘要,保留以下关键信息:
1. 用户的原始请求和目标
2. 已完成的操作和结果
3. 当前的工作状态
4. 未完成的任务
5. 重要的文件路径和代码片段
6. 错误信息和解决方案

不要保留:
- 冗余的工具调用细节
- 重复的文件内容
- 中间的探索性尝试(除非结论重要)

手动压缩命令

用户可以通过 /compact 斜杠命令手动触发压缩:

1
2
3
4
5
6
7
8
/compact [instructions]

├── 无参数 → 使用默认压缩指令
├── 有参数 → 将参数作为额外的压缩指令
│ 例如:/compact 保留所有文件路径和错误信息

└── 执行 compactConversation()
└── 与 autoCompact 相同的流程

Token 计数与预算

src/utils/countTokens.ts 提供精确的 token 计数:

1
2
3
4
5
6
7
8
9
10
11
// Token 计数的层级
countTokens(messages) // 消息级别
countToolResultTokens(result) // 工具结果级别
estimateTokenCount(text) // 文本估算(快速,不精确)

// 预算分配
const TOKEN_BUDGET = {
systemPrompt: 0.15, // 系统提示占 15%
toolSchemas: 0.10, // 工具 schema 占 10%
conversation: 0.75, // 对话历史占 75%(可压缩)
}

normalizeMessages

src/utils/messages/normalizeMessages.ts 是管线的最后一步——将内部消息格式转换为 API 格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
normalizeMessages 转换规则

├── 合并连续的同角色消息
│ └── API 要求 user/assistant 交替

├── 移除空消息
│ └── 压缩后可能产生空消息

├── 处理 cache_control
│ ├── 标记 ephemeral breakpoint
│ └── 用于 Prompt Cache 优化

├── 处理 tool_result 格式
│ ├── 内部格式 → API 格式
│ ├── 截断超长结果
│ └── 添加 is_error 标记

└── 注入 prependUserContext
├── 用户上下文(文件列表、git 状态等)
└── 附加到第一条 user 消息

Prompt-Too-Long 三级恢复

当 API 返回 413 prompt-too-long 错误时,系统按以下顺序尝试恢复:

SUMMARIZE_TOOL_RESULTS 模式

SUMMARIZE_TOOL_RESULTS feature flag 启用时,system prompt 中注入额外指令,要求模型在工具结果过长时自行生成摘要。这是一种"模型侧压缩"——不依赖后处理管线,而是让模型在生成响应时就进行信息压缩。

1
2
3
SUMMARIZE_TOOL_RESULTS 指令(注入 system prompt):
"当工具结果超过 2000 tokens 时,在你的响应中只引用关键信息,
不要逐字复述工具输出。用 <summary> 标签包裹你的摘要。"

这与五层管线互补——管线处理历史消息,SUMMARIZE_TOOL_RESULTS 处理当前轮次的新结果。

Session Memory 与压缩的协作

src/services/compact/sessionMemoryCompact.ts(21.18KB)实现了压缩过程中的会话记忆保留。当 autoCompact 触发时,大量对话细节被摘要替代,但某些信息对当前任务至关重要。Session Memory 在压缩前提取这些关键信息,压缩后重新注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Session Memory 与 Compact 的协作

├── 压缩前
│ ├── sessionMemoryCompact 分析即将被压缩的消息
│ ├── 提取关键上下文:
│ │ ├── 用户的原始请求和目标
│ │ ├── 当前工作状态(正在做什么、进度如何)
│ │ ├── 本次会话中用户提出的特殊约束
│ │ ├── 未完成的任务列表
│ │ └── 关键的文件路径和代码位置
│ └── 存储为 SessionMemory 对象

├── 压缩中
│ ├── compactConversation() 正常执行 LLM 摘要
│ └── 旧消息被替换为压缩摘要

└── 压缩后
├── Session Memory 作为独立消息注入
├── 位置:紧跟在压缩摘要之后
├── 格式:<session_memory> XML 标签
└── 确保模型在压缩后仍然知道关键上下文

这解决了一个核心矛盾:压缩需要丢弃细节以节省空间,但当前任务需要某些细节才能继续。Session Memory 是这个矛盾的折中方案——只保留对当前任务最关键的信息。

compact_boundary 标记

压缩后的摘要消息带有 compact_boundary 标记,用于标识压缩边界。这个标记有多个用途:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
compact_boundary 的作用

├── 1. 防止重复压缩
│ └── autoCompact 不会压缩 compact_boundary 之后的消息
│ (这些是压缩后新产生的消息,不应该被再次压缩)

├── 2. 消息分区
│ ├── compact_boundary 之前 = 压缩摘要(不可展开)
│ └── compact_boundary 之后 = 原始消息(可压缩)

├── 3. Token 计数基准
│ └── autoCompact 只计算 compact_boundary 之后的消息 token 数
│ (摘要部分的 token 数是固定的,不需要重复计算)

└── 4. Session Memory 注入点
└── Session Memory 注入在 compact_boundary 之后的第一条消息之前

Prompt Cache 断裂检测

src/services/api/promptCacheBreakDetection.ts(26.38KB)实现了 Prompt Cache 命中率的实时监控。当检测到 cache 命中率异常下降时,系统会尝试诊断原因并自动修复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Prompt Cache 断裂检测

├── 监控指标
│ ├── cache_creation_input_tokens(新创建的缓存 token 数)
│ ├── cache_read_input_tokens(从缓存读取的 token 数)
│ └── cache_hit_rate = read / (read + creation)

├── 断裂检测
│ ├── 连续 N 次 API 调用的 cache_hit_rate < 阈值
│ ├── 或 cache_creation_input_tokens 突然大幅增加
│ └── → 触发断裂告警

├── 常见断裂原因
│ ├── System Prompt 动态部分变化(记忆更新、MCP 工具变化)
│ ├── 工具 Schema 顺序变化(MCP 工具增减)
│ ├── 消息格式变化(normalizeMessages 的行为变化)
│ └── API 端侧缓存过期

└── 自动修复
├── 重新排序工具 Schema(恢复稳定顺序)
├── 冻结动态 Prompt 部分(延迟更新到下一轮)
└── 记录诊断信息到遥测

Token 估算算法

src/services/tokenEstimation.ts(16.97KB)提供了多种精度的 token 估算方法:

方法 精度 速度 适用场景
countTokens() 精确 慢(~1ms/消息) 压缩决策、预算检查
estimateTokenCount() 近似(±10%) 快(~0.01ms) 快速预检、UI 显示
estimateToolResultTokens() 近似 工具结果预算

estimateTokenCount() 使用简单的字符数/4 启发式——对英文文本误差约 ±10%,对代码误差约 ±15%。这个精度足够用于快速预检(如判断是否需要触发 autoCompact),但不够用于精确的预算计算。

精确计数的缓存countTokens() 的结果会被缓存——对于未变化的消息,不需要重复计数。缓存 key 是消息内容的 hash。这在长会话中显著减少了 token 计数的开销——只有新增的消息需要计数,历史消息使用缓存值。

contentReplacementState 持久化

applyToolResultBudget 的替换记录通过 contentReplacementState 持久化到会话文件中。这支持了会话恢复——当用户恢复一个之前的会话时,系统可以重放替换记录,将磁盘文件引用正确地映射回原始工具结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contentReplacementState 持久化规则

├── 持久化条件
│ ├── 只有 agent:* 和 repl_main_thread 来源的查询持久化
│ └── 临时的 runForkedAgent 调用不持久化

├── 持久化内容
│ ├── 替换映射(tool_use_id → 磁盘文件路径)
│ ├── 原始结果的 token 数
│ └── 替换时间戳

└── 恢复流程
├── 加载会话文件
├── 重建 contentReplacementState
└── 后续的 applyToolResultBudget 可以识别已替换的结果

六、记忆系统(Memory System)

Claude Code 的记忆系统实现了跨会话的知识持久化,是整个产品"越用越懂你"体验的核心。系统由五个子模块组成:CLAUDE.md 文件层级(项目约定)、memdir 结构化记忆(用户偏好与知识)、Session Memory 会话记忆(单次会话内的短期记忆)、团队记忆同步(跨设备/跨成员共享)、Auto Dream 记忆整固(后台记忆优化)。

核心文件

文件 大小 说明
src/utils/claudemd.ts 46.75 KB CLAUDE.md 加载、解析、@include 展开
src/memdir/memdir.ts 21.17 KB memdir 记忆目录核心操作
src/memdir/memoryTypes.ts 22.59 KB 记忆类型定义(schema、标签、元数据)
src/memdir/findRelevantMemories.ts 5.32 KB 相关记忆检索(语义匹配)
src/memdir/paths.ts 10.69 KB 记忆路径管理(项目哈希、目录结构)
src/memdir/teamMemPaths.ts 11.70 KB 团队记忆路径
src/memdir/teamMemPrompts.ts 5.96 KB 团队记忆 prompt 模板
src/services/extractMemories/extractMemories.ts 21.78 KB 自动记忆提取引擎
src/services/extractMemories/prompts.ts 7.64 KB 提取 prompt 模板
src/services/teamMemorySync/index.ts 44.34 KB 团队记忆同步服务
src/services/teamMemorySync/secretScanner.ts 9.55 KB 秘密扫描器(防泄漏)
src/services/autoDream/autoDream.ts 11.31 KB Auto Dream 记忆整固
src/services/SessionMemory/sessionMemory.ts 16.66 KB 会话记忆管理
src/services/SessionMemory/prompts.ts 12.65 KB 会话记忆 prompt
src/services/compact/sessionMemoryCompact.ts 21.18 KB 会话记忆压缩

CLAUDE.md 四级加载体系

记忆文件按作用域分为四级,从全局到局部逐级加载。src/utils/claudemd.ts(46.75KB)是这个体系的核心实现。

级别 路径 作用域 说明
1. 用户级 ~/.claude/CLAUDE.md 全局 用户个人偏好,跨所有项目生效
2. 项目根级 {projectRoot}/CLAUDE.md 项目 项目级约定,团队共享(checked in)
3. 子目录级 {cwd}/CLAUDE.md 目录 子模块/子目录特定指令
4. 父目录级 {parent}/.claude/CLAUDE.md 祖先 向上遍历直到文件系统根

加载顺序:用户级 → 父目录级(从根到当前目录)→ 项目根级 → 子目录级。后加载的内容优先级更高——子目录的 CLAUDE.md 可以覆盖项目根的指令。

claudemd.ts 的完整解析流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
loadClaudeMd(projectRoot, cwd, userHome)

├── 1. 发现阶段
│ ├── findClaudeMdFiles(userHome) // ~/.claude/CLAUDE.md
│ ├── findClaudeMdFiles(projectRoot) // {projectRoot}/CLAUDE.md
│ ├── findClaudeMdFiles(cwd) // {cwd}/CLAUDE.md(如果 cwd ≠ projectRoot)
│ ├── walkAncestors(projectRoot → /) // 向上遍历所有祖先目录
│ │ └── 每个目录检查 .claude/CLAUDE.md 和 CLAUDE.md
│ └── 去重(同一文件不加载两次)

├── 2. 读取阶段
│ ├── 对每个发现的文件:
│ │ ├── readFile(path, 'utf-8')
│ │ ├── 检查文件大小 ≤ MAX_CLAUDEMD_SIZE(100KB)
│ │ └── 超过大小限制 → 截断 + 警告
│ └── Trust 检查
│ ├── 用户级文件 → 始终信任
│ ├── 项目级文件 → 需要 workspace trust
│ └── 未信任 → 跳过(不加载)

├── 3. @include 展开阶段
│ ├── 正则匹配 /^@(.+)$/gm
│ ├── 对每个 @include:
│ │ ├── 解析路径(相对于当前 CLAUDE.md 所在目录)
│ │ ├── 安全检查
│ │ │ ├── 路径必须在工作目录内
│ │ │ ├── 不能是符号链接指向外部
│ │ │ └── 不能包含 .. 逃逸
│ │ ├── 递归深度检查(最大 5 层)
│ │ ├── 读取文件内容
│ │ └── 递归展开嵌套的 @include
│ └── 文件不存在 → 静默忽略(不报错,不中断)

├── 4. 格式化阶段
│ ├── 每个来源用 XML 标签包裹
│ │ ├── <user_instructions source="~/.claude/CLAUDE.md">
│ │ ├── <project_instructions source="{projectRoot}/CLAUDE.md">
│ │ └── <directory_instructions source="{cwd}/CLAUDE.md">
│ ├── 标签中包含来源路径(便于模型引用)
│ └── 拼接为最终字符串

└── 5. Token 预算检查
├── 计算总 token 数
├── 超过预算 → 按优先级裁剪
└── 返回最终的记忆 prompt

@include 的实际使用场景

1
2
3
4
5
6
7
8
9
# 项目 CLAUDE.md 示例
@docs/coding-standards.md # 引用编码规范文档
@.claude/prompts/review-checklist.md # 引用代码审查清单
@scripts/README.md # 引用脚本说明

## 项目特定规则
- 使用 TypeScript strict mode
- 所有 API 端点必须有 OpenAPI 注释
- 提交消息遵循 Conventional Commits

CLAUDE.md 的安全模型

项目级 CLAUDE.md 是一个潜在的攻击向量——恶意仓库可以在 CLAUDE.md 中注入指令,让模型执行危险操作。Claude Code 通过以下机制防御:

  1. Workspace Trust:项目级 CLAUDE.md 只在用户确认信任工作区后才加载。首次打开项目时,Trust Dialog 会显示 CLAUDE.md 的内容预览。
  2. @include 沙箱:@include 只能引用工作目录内的文件,不能逃逸到系统目录。
  3. 大小限制:单个文件 100KB,防止通过超大文件消耗上下文窗口。
  4. InstructionsLoaded Hook:企业可以通过 Hook 审计加载的指令内容。

Memdir 结构化记忆

src/memdir/memdir.ts(21.17KB)实现了结构化的记忆存储系统。与 CLAUDE.md 的纯文本记忆不同,memdir 提供了类型化、可检索、可同步的记忆条目。

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
~/.claude/memory/
├── user/ // 用户级记忆(全局)
│ ├── preferences.md // 用户偏好
│ ├── coding-style.md // 编码风格
│ └── tools.md // 工具使用习惯

├── project-{hash}/ // 项目级记忆(按项目哈希隔离)
│ ├── architecture.md // 架构决策
│ ├── conventions.md // 项目约定
│ ├── common-errors.md // 常见错误与解决方案
│ └── dependencies.md // 依赖选择理由

├── team/ // 团队记忆(跨成员共享)
│ ├── shared-conventions.md // 团队共享约定
│ └── onboarding.md // 新成员引导

└── .index.json // 记忆索引(元数据缓存)

项目哈希计算src/memdir/paths.ts 中的 getProjectHash() 使用项目根目录的绝对路径计算 SHA-256 哈希的前 12 位。这保证了不同项目的记忆完全隔离,即使项目名称相同。

记忆类型系统

src/memdir/memoryTypes.ts(22.59KB)定义了记忆条目的完整类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
interface MemoryEntry {
// 元数据
id: string // 唯一标识符(UUID)
type: MemoryType // 记忆类型
scope: 'user' | 'project' | 'team' // 作用域

// 内容
title: string // 标题(用于检索)
content: string // 正文(Markdown 格式)
tags: string[] // 标签(用于分类和检索)

// 时间戳
createdAt: string // 创建时间(ISO 8601)
updatedAt: string // 最后更新时间
lastAccessedAt: string // 最后访问时间(用于 LRU 淘汰)

// 来源追踪
source: 'auto' | 'manual' | 'dream' | 'team-sync' // 创建来源
sessionId?: string // 创建时的会话 ID
confidence: number // 置信度(0-1,auto 提取的记忆通常 < 1)
}

type MemoryType =
| 'preference' // 用户偏好("我喜欢用 tabs")
| 'convention' // 项目约定("使用 camelCase")
| 'architecture' // 架构决策("使用微服务架构")
| 'error-solution' // 错误解决方案
| 'workflow' // 工作流程
| 'context' // 上下文信息(项目背景、团队结构等)

记忆的 CRUD 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
memdir CRUD 操作

├── createMemory(entry)
│ ├── 生成 UUID
│ ├── 写入 Markdown 文件(frontmatter + content)
│ ├── 更新 .index.json
│ └── 触发团队同步(如果 scope === 'team')

├── readMemory(id)
│ ├── 从 .index.json 查找路径
│ ├── 读取文件内容
│ ├── 解析 frontmatter
│ └── 更新 lastAccessedAt

├── updateMemory(id, updates)
│ ├── 读取现有内容
│ ├── 合并更新
│ ├── 写入文件
│ ├── 更新 .index.json
│ └── 触发团队同步

├── deleteMemory(id)
│ ├── 删除文件
│ ├── 从 .index.json 移除
│ └── 触发团队同步(标记为已删除)

└── listMemories(filter?)
├── 读取 .index.json
├── 按 filter 过滤(scope、type、tags)
└── 按 updatedAt 降序排列

findRelevantMemories 相关性检索

src/memdir/findRelevantMemories.ts(5.32KB)实现了基于当前对话上下文的记忆检索——不是加载所有记忆,而是只加载与当前任务相关的记忆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
findRelevantMemories(context) 检索流程

├── 1. 构建查询向量
│ ├── 提取当前对话的关键词
│ ├── 提取最近工具调用涉及的文件路径
│ ├── 提取用户最近的问题/指令
│ └── 组合为查询字符串

├── 2. 候选记忆收集
│ ├── 加载 user/ 目录下所有记忆
│ ├── 加载 project-{hash}/ 目录下所有记忆
│ └── 加载 team/ 目录下所有记忆

├── 3. 相关性评分
│ ├── 标签匹配得分(tags 与查询关键词的交集)
│ ├── 标题匹配得分(title 与查询的模糊匹配)
│ ├── 内容匹配得分(content 中关键词出现频率)
│ ├── 时间衰减因子(最近访问的记忆得分更高)
│ └── 来源权重(manual > auto > dream)

├── 4. 排序与截断
│ ├── 按综合得分降序排列
│ ├── 取 top-K 条记忆(K 由 token 预算决定)
│ └── 确保总 token 数不超过 MEMORY_TOKEN_BUDGET

└── 5. 返回
└── 格式化为 <relevant_memories> XML 标签

与 Memory Prefetch 的集成

在代理循环中,findRelevantMemories() 通过 prefetch 机制异步执行——在工具执行期间后台检索相关记忆,不阻塞主循环。检索结果作为附件消息注入到下一次 API 调用中:

1
2
3
4
5
6
7
8
9
10
11
12
13
// query.ts 中的 Memory Prefetch
using pendingMemoryPrefetch = startRelevantMemoryPrefetch(
state.messages, state.toolUseContext
)

// 工具执行完成后,消费 prefetch 结果
if (pendingMemoryPrefetch?.settledAt !== null && consumedOnIteration === -1) {
const memoryAttachments = filterDuplicateMemoryAttachments(
await pendingMemoryPrefetch.promise,
toolUseContext.readFileState // 过滤模型已 Read/Write/Edit 的文件
)
// memoryAttachments 作为 user 消息注入
}

去重逻辑filterDuplicateMemoryAttachments() 过滤掉模型已经通过 Read/Write/Edit 工具访问过的文件——如果模型刚刚读取了 CLAUDE.md,就不需要再通过记忆系统注入相同的内容。


Auto Memory 自动记忆提取

src/services/extractMemories/extractMemories.ts(21.78KB)实现了自动记忆提取——在会话结束时,LLM 分析对话历史,提取值得跨会话记住的信息。

触发条件

1
2
3
4
5
6
7
8
Auto Memory 触发条件(全部满足才触发)

├── 会话结束时(用户退出、/exit、或 headless 模式完成)
├── CLAUDE_CODE_SIMPLE !== '1'(bare 模式禁用)
├── feature('AUTO_MEMORY') 启用
├── 对话长度 > MIN_CONVERSATION_LENGTH(避免短对话触发)
├── 距离上次提取 > EXTRACT_COOLDOWN(防止频繁提取)
└── 用户未禁用自动记忆(settings.json 中的 autoMemory 配置)

提取流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
extractMemories(conversationHistory)

├── 1. 对话预处理
│ ├── 过滤系统消息和工具调用细节
│ ├── 保留用户消息和模型的关键响应
│ ├── 截断到最大 token 限制(避免超长对话的提取成本过高)
│ └── 标记用户明确表达偏好的消息("我喜欢..."、"请总是..."、"不要...")

├── 2. LLM 提取调用
│ ├── 使用 Haiku 模型(成本低、速度快)
│ ├── 注入提取 prompt(src/services/extractMemories/prompts.ts)
│ └── 要求返回结构化 JSON

├── 3. 提取 Prompt 的关键指令
│ ├── "分析对话历史,提取值得跨会话记住的信息"
│ ├── 关注类别:
│ │ ├── 用户明确表达的偏好("我喜欢..."、"请总是..."、"不要...")
│ │ ├── 项目特定的约定(命名规范、目录结构、依赖选择)
│ │ ├── 反复出现的工作模式(用户总是先写测试、总是要求代码审查)
│ │ ├── 重要的错误及其解决方案(特别是环境特定的问题)
│ │ └── 架构决策及其理由
│ ├── 不要提取:
│ │ ├── 临时性的任务细节("修复这个 bug")
│ │ ├── 具体的代码片段(除非是通用模板)
│ │ ├── 一次性的操作步骤
│ │ └── 已经在 CLAUDE.md 中记录的内容
│ └── 输出格式:JSON 数组,每条包含 type、title、content、tags、confidence

├── 4. 去重与冲突检测
│ ├── 加载现有记忆的 .index.json
│ ├── 对每条候选记忆:
│ │ ├── 标题相似度检查(Levenshtein 距离 < 阈值 → 视为重复)
│ │ ├── 内容相似度检查(关键词重叠率 > 阈值 → 视为重复)
│ │ ├── 标签完全匹配 → 可能是同一主题的更新
│ │ └── 冲突检测(新记忆与旧记忆矛盾 → 标记为更新)
│ ├── 重复 → 跳过
│ ├── 更新 → 合并到现有记忆
│ └── 新信息 → 创建新记忆条目

└── 5. 写入
├── 调用 createMemory() 或 updateMemory()
├── source 标记为 'auto'
├── confidence 使用 LLM 返回的置信度
└── 后台执行,不阻塞用户退出

Session Memory 会话记忆

src/services/SessionMemory/sessionMemory.ts(16.66KB)实现了单次会话内的短期记忆——与 memdir 的跨会话长期记忆不同,Session Memory 只在当前会话内有效,用于在压缩(compact)后保留关键的工作上下文。

Session Memory 的核心问题:当 autoCompact 压缩对话历史时,大量细节被摘要替代。但某些信息对当前任务至关重要——例如"用户要求所有函数都加 JSDoc"、“当前正在重构 auth 模块”。Session Memory 在压缩前提取这些关键信息,压缩后重新注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Session Memory 生命周期

├── 1. 提取(压缩前)
│ ├── sessionMemoryCompact.ts 在 compactConversation() 中调用
│ ├── 分析即将被压缩的消息
│ ├── 提取关键上下文:
│ │ ├── 用户的原始请求和目标
│ │ ├── 当前的工作状态(正在做什么、做到哪里了)
│ │ ├── 重要的约束和偏好(本次会话中用户提出的)
│ │ ├── 未完成的任务列表
│ │ └── 关键的文件路径和代码位置
│ └── 存储为 SessionMemory 对象

├── 2. 注入(压缩后)
│ ├── 在压缩摘要之后注入 Session Memory
│ ├── 格式:<session_memory> XML 标签
│ ├── 位置:紧跟在压缩摘要消息之后
│ └── 确保模型在压缩后仍然知道关键上下文

├── 3. 更新(每次压缩时)
│ ├── 新的压缩 → 新的 Session Memory 提取
│ ├── 与旧的 Session Memory 合并
│ ├── 去除已完成的任务
│ └── 更新工作状态

└── 4. 过期
└── 会话结束时自动丢弃(不持久化到 memdir)

Session Memory Promptsrc/services/SessionMemory/prompts.ts,12.65KB):

1
2
3
4
5
6
7
8
9
10
11
12
提取 Session Memory 的 prompt 指令:
"你正在分析一段即将被压缩的对话历史。请提取以下关键信息,
这些信息将在压缩后注入,帮助你继续工作:

1. 用户的核心目标(一句话总结)
2. 当前工作状态(正在做什么、进度如何)
3. 重要的约束(用户在本次会话中提出的特殊要求)
4. 未完成的任务(列表形式)
5. 关键的文件路径(正在编辑或需要关注的文件)
6. 重要的错误和解决方案(如果有)

格式要求:简洁、结构化、可直接作为上下文使用。"

团队记忆同步

src/services/teamMemorySync/index.ts(44.34KB)实现了跨设备、跨团队成员的记忆同步。

同步架构

同步流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
teamMemorySync() 完整流程

├── 1. 触发条件
│ ├── 会话开始时(拉取远程变更)
│ ├── 记忆变更时(推送本地变更)
│ ├── 定期轮询(每 5 分钟检查远程变更)
│ └── 手动触发(/memory sync 命令)

├── 2. 拉取(Pull)
│ ├── GET /v1/memory-sync?since={lastSyncTimestamp}
│ ├── 获取自上次同步以来的远程变更
│ ├── 对每条远程变更:
│ │ ├── 本地不存在 → 创建
│ │ ├── 本地存在且远程更新 → 合并
│ │ ├── 本地已删除 → 跳过
│ │ └── 冲突(双方都修改) → 使用 LWW(Last Writer Wins)策略
│ └── 更新 lastSyncTimestamp

├── 3. 推送(Push)
│ ├── 收集自上次同步以来的本地变更
│ ├── 对每条变更执行 Secret Scanner
│ │ └── 检测到秘密 → 阻止推送 + 警告用户
│ ├── POST /v1/memory-sync
│ │ ├── 发送变更集(创建/更新/删除)
│ │ └── 附带本地时间戳
│ └── 更新 lastSyncTimestamp

└── 4. 冲突解决
├── LWW(Last Writer Wins):时间戳更新的版本胜出
├── 如果时间戳相同 → 内容更长的版本胜出
└── 删除操作优先级最低(防止误删)

Secret Scanner 秘密扫描器

src/services/teamMemorySync/secretScanner.ts(9.55KB)在记忆同步前扫描内容,防止敏感信息泄漏到远程存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Secret Scanner 检测规则

├── API Key 模式
│ ├── AWS: AKIA[0-9A-Z]{16}
│ ├── GitHub: ghp_[a-zA-Z0-9]{36}
│ ├── Anthropic: sk-ant-[a-zA-Z0-9-]+
│ ├── OpenAI: sk-[a-zA-Z0-9]{48}
│ └── 通用: [a-zA-Z0-9]{32,} 在 key/token/secret 上下文中

├── 私钥模式
│ ├── -----BEGIN RSA PRIVATE KEY-----
│ ├── -----BEGIN EC PRIVATE KEY-----
│ └── -----BEGIN OPENSSH PRIVATE KEY-----

├── 连接字符串模式
│ ├── postgresql://user:password@host
│ ├── mongodb://user:password@host
│ └── redis://user:password@host

├── 环境变量模式
│ ├── DATABASE_URL=...
│ ├── SECRET_KEY=...
│ └── PASSWORD=...

└── 处理策略
├── 检测到秘密 → 阻止同步
├── 记录警告日志
├── 通知用户("记忆中包含敏感信息,已阻止同步")
└── 建议用户移除敏感信息后重试

Auto Dream 记忆整固

src/services/autoDream/autoDream.ts(11.31KB)实现了一个后台"做梦"机制——在空闲时间运行一个子代理,对现有记忆进行整理、合并、优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Auto Dream 流程

├── 触发条件
│ ├── 会话空闲超过阈值(用户长时间未输入)
│ ├── 或会话结束后的后台任务
│ ├── 且 memdir 中记忆条目数 > MIN_MEMORIES_FOR_DREAM
│ └── 且距离上次 Dream > DREAM_COOLDOWN

├── Dream 子代理
│ ├── 使用 Haiku 模型(低成本)
│ ├── 加载所有现有记忆
│ ├── 分析记忆质量和组织结构
│ └── 执行以下操作:

├── 整固操作
│ ├── 合并重复记忆
│ │ └── 多条关于同一主题的记忆 → 合并为一条更全面的
│ ├── 更新过时记忆
│ │ └── 与最近的对话历史对比,更新不再准确的信息
│ ├── 提升记忆质量
│ │ └── 模糊的记忆 → 添加更多细节和上下文
│ ├── 调整标签
│ │ └── 统一标签命名,添加缺失的标签
│ └── 淘汰低价值记忆
│ └── 长时间未访问 + 低置信度 → 标记为候选删除

└── 结果
├── 更新后的记忆写回 memdir
├── source 标记为 'dream'
└── 记录 Dream 日志(用于审计)

loadMemoryPrompt 的完整组装

src/utils/memory.ts 中的 loadMemoryPrompt() 将所有记忆源组装为 system prompt 的一部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
loadMemoryPrompt() 组装顺序

├── 1. CLAUDE.md 层级
│ ├── 用户级 ~/.claude/CLAUDE.md(含 @include 展开)
│ ├── 父目录链 CLAUDE.md(从根到当前目录)
│ ├── 项目根级 {projectRoot}/CLAUDE.md(含 @include 展开)
│ └── 子目录级 {cwd}/CLAUDE.md(如果 cwd ≠ projectRoot)

├── 2. Memdir 结构化记忆
│ ├── user/ 目录下的相关记忆(findRelevantMemories 筛选)
│ ├── project-{hash}/ 目录下的相关记忆
│ └── team/ 目录下的相关记忆

├── 3. Session Memory(如果存在)
│ └── 上一次压缩后保留的会话记忆

├── 4. 格式化输出
│ ├── <user_instructions>...</user_instructions>
│ ├── <project_instructions>...</project_instructions>
│ ├── <relevant_memories>...</relevant_memories>
│ └── <session_memory>...</session_memory>

└── 5. Token 预算裁剪
├── 总预算:~16K tokens
├── 超过预算时按优先级裁剪:
│ ├── 结构化记忆(最旧的条目先裁剪)
│ ├── 父目录链记忆(最远的祖先先裁剪)
│ ├── 项目级记忆(尾部裁剪)
│ └── 用户级记忆(尾部裁剪)
└── CLAUDE.md 内容优先级高于 memdir 记忆

记忆的 Token 预算

记忆内容有严格的 token 预算限制,防止过大的记忆文件占满上下文窗口:

1
2
3
4
5
6
7
const MEMORY_TOKEN_BUDGET = {
userInstructions: 4000, // 用户级 CLAUDE.md 最大 4K tokens
projectInstructions: 8000, // 项目级 CLAUDE.md 最大 8K tokens
relevantMemories: 4000, // 相关记忆最大 4K tokens
sessionMemory: 2000, // 会话记忆最大 2K tokens
total: 16000, // 总计最大 16K tokens
}

预算分配策略CLAUDE.md 的预算是固定的(用户和项目各自有上限),memdir 记忆的预算是弹性的——如果 CLAUDE.md 使用的 token 较少,剩余的预算会分配给 memdir 记忆。


/memory 命令

用户可以通过 /memory 斜杠命令管理记忆:

1
2
3
4
5
6
7
8
9
10
11
/memory                        → 显示当前加载的所有记忆来源和 token 使用情况
/memory edit → 打开编辑器编辑项目级 CLAUDE.md
/memory edit --global → 编辑用户级 ~/.claude/CLAUDE.md
/memory edit --project → 编辑项目级 CLAUDE.md
/memory list → 列出 memdir 中的所有记忆条目
/memory list --project → 只列出项目级记忆
/memory list --tags typescript → 按标签过滤
/memory clear → 清除所有结构化记忆
/memory clear --project → 只清除项目级结构化记忆
/memory sync → 手动触发团队记忆同步
/memory dream → 手动触发 Auto Dream 整固

InstructionsLoaded Hook

CLAUDE.md 加载完成后,触发 InstructionsLoaded Hook,允许企业审计和控制记忆内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Hook 输入
{
event: 'InstructionsLoaded',
data: {
sources: [
{ path: '~/.claude/CLAUDE.md', content: '...', tokens: 1200 },
{ path: '/project/CLAUDE.md', content: '...', tokens: 2300 },
],
totalTokens: 3500,
memoryEntries: 12, // memdir 中加载的记忆条目数
teamMemories: 3, // 团队记忆条目数
}
}

// Hook 可以返回
{
suppressOutput: true, // 阻止某些记忆加载
systemMessage: '...', // 注入额外的系统消息
}

记忆与 Prompt Cache 的交互

记忆内容位于 system prompt 的动态区域SYSTEM_PROMPT_DYNAMIC_BOUNDARY 之后),这意味着记忆内容的变化不会破坏静态区域的 Prompt Cache。但记忆内容本身的变化会导致动态区域的 cache miss。

优化策略

  • 会话内缓存:记忆内容在会话内缓存,只在 settingsChangeDetector 检测到 CLAUDE.md 文件变化时重新加载
  • @include 缓存:@include 文件的内容通过文件 mtime 检测变化,未变化时使用缓存
  • memdir 索引缓存.index.json 的 hash 用于快速检测记忆变化,避免每次都遍历目录
  • Memory Prefetch 去重filterDuplicateMemoryAttachments() 避免注入模型已经通过工具访问过的文件内容
  • 稳定排序:记忆条目按 ID 排序注入,保证相同的记忆集合产生相同的 prompt 文本,最大化 cache 命中率

记忆注入策略:System Prompt vs Tool Result

记忆有两种注入方式,适用于不同场景:

注入方式 时机 内容 适用场景
System Prompt 每次 API 调用 CLAUDE.md + 高优先级记忆 始终需要的项目约定和用户偏好
Tool Result 工具执行后的附件 相关记忆(findRelevantMemories) 与当前任务相关的上下文记忆

System Prompt 注入的记忆是"始终可见"的——模型在每次响应中都能看到。Tool Result 注入的记忆是"按需可见"的——只在相关时才出现,节省上下文空间。

这种双轨策略平衡了记忆覆盖率上下文效率:核心约定通过 System Prompt 始终生效,长尾知识通过 Tool Result 按需注入。


七、多代理架构(Multi-Agent)

Claude Code 支持多种代理生成模式,从简单的子代理(AgentTool)到完整的团队协作(Swarm),实现了从单任务委派到多代理并行协作的完整谱系。

四种代理生成模式

模式 进程模型 上下文共享 通信方式 适用场景
AgentTool 同进程 共享 prompt cache 函数调用 简单子任务委派
Fork 子进程 继承父上下文 IPC 需要隔离的并行任务
Swarm 独立进程 无共享 消息队列 多代理协作
Task 后台进程 无共享 轮询/通知 长时间异步任务

AgentTool 子代理

src/tools/AgentTool/AgentTool.ts 是最常用的子代理模式。主代理通过 AgentTool 工具调用创建子代理,子代理在同一进程内执行。

子代理的 Prompt 构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
AgentTool 子代理 Prompt

├── System Prompt
│ ├── 继承主代理的 system prompt(静态部分)
│ ├── 添加子代理特定指令
│ │ ├── "你是一个子代理,专注于完成特定任务"
│ │ ├── "完成后使用 TaskOutput 返回结果"
│ │ └── "不要偏离分配的任务"
│ └── 工具集限制(可能比主代理少)

├── 初始消息
│ ├── 主代理的任务描述
│ ├── 相关上下文(文件路径、代码片段)
│ └── 期望的输出格式

└── 约束
├── maxTurns: 受限(防止无限循环)
├── 权限: 继承主代理的权限模式
└── 工具: 可配置的工具子集

Prompt Cache 共享:AgentTool 子代理与主代理共享 system prompt 的静态部分,这意味着子代理的首次 API 调用可以命中主代理的 Prompt Cache——节省约 15% 的 token 成本。

递归防护

1
2
3
4
5
6
7
8
9
10
// 子代理递归深度限制
const MAX_AGENT_DEPTH = 3 // 最大嵌套深度

// AgentTool 内部检查
if (currentDepth >= MAX_AGENT_DEPTH) {
return {
type: 'error',
error: `Maximum agent nesting depth (${MAX_AGENT_DEPTH}) exceeded`
}
}

内置代理类型

通过 --agents 参数或 .claude/agents/ 目录定义的代理模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
代理定义格式(.claude/agents/*.md 或 .claude/agents/*.yml)

├── Markdown 格式
│ ├── 文件名 = 代理名称
│ ├── 文件内容 = 代理的 system prompt 补充
│ └── 通过 @include 引用其他文件

└── YAML 格式
├── name: 代理名称
├── description: 代理描述
├── prompt: 代理的 system prompt 补充
├── tools: 可用工具列表
├── maxTurns: 最大迭代次数
└── model: 使用的模型(可覆盖)

代理发现getAgentDefinitionsWithOverrides() 在启动时扫描 .claude/agents/ 目录,将每个文件解析为代理定义。用户可以通过 @agent-name 在对话中引用特定代理。

Fork 子代理

Fork 模式通过 child_process.fork() 创建独立的 Node.js 进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Fork 子代理生命周期

├── 创建
│ ├── fork() 创建子进程
│ ├── 传递父代理的上下文快照
│ │ ├── 当前消息历史
│ │ ├── 工作目录
│ │ ├── 环境变量
│ │ └── 权限配置
│ └── 子进程独立运行 query() 循环

├── 通信
│ ├── IPC channel(process.send / process.on('message'))
│ ├── 进度报告(子 → 父)
│ ├── 权限请求(子 → 父 → 用户 → 父 → 子)
│ └── 结果返回(子 → 父)

├── 隔离
│ ├── 独立的内存空间
│ ├── 独立的文件句柄
│ ├── 独立的 API 连接
│ └── 崩溃不影响父进程

└── 终止
├── 正常完成 → 返回结果
├── 超时 → SIGTERM → SIGKILL
└── 父进程退出 → 子进程自动终止

Swarm 团队协作

src/utils/swarm/ 实现了多代理团队协作模式(feature: AGENT_SWARMS)。

团队创建

SendMessageTool:团队成员之间通过 SendMessageTool 通信。消息通过共享的消息队列传递,每个代理在自己的循环中轮询新消息。

TeamDeleteTool:销毁团队,终止所有成员进程,清理资源。

协调器模式:当 awaitAutomatedChecksBeforeDialog 为 true 时,权限请求先经过协调器的自动化检查(classifier + hooks),只有未通过的请求才提交给用户。这减少了多代理场景下的权限提示频率。

Task 后台任务

Task 系统(feature: TASKS)支持长时间运行的后台任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Task 生命周期

├── TaskCreateTool
│ ├── 创建任务定义
│ ├── 分配唯一 ID
│ └── 启动后台进程

├── 执行中
│ ├── 独立的 query() 循环
│ ├── 定期写入进度到 TaskOutput
│ ├── 可通过 TaskGetTool 查询状态
│ └── 可通过 TaskStopTool 停止

├── 状态
│ ├── pending → 等待执行
│ ├── running → 执行中
│ ├── completed → 已完成
│ ├── failed → 失败
│ └── stopped → 被停止

└── 结果
├── TaskOutputTool 写入最终结果
├── TaskGetTool 读取结果
└── TaskListTool 列出所有任务

Todo V2:当 isTodoV2Enabled() 为 true 时,Task 系统升级为 Todo V2——支持任务依赖、优先级、标签等更丰富的任务管理功能。

后台会话管理

src/entrypoints/cli.tsx 中的 pslogsattachkill 命令(feature: BG_SESSIONS)管理后台运行的代理会话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
后台会话命令

├── claude ps → 列出所有后台会话
│ └── 显示 ID、状态、运行时间、任务描述

├── claude logs <id> → 查看会话日志
│ └── 实时流式输出

├── claude attach <id> → 附加到后台会话
│ └── 恢复交互式控制

├── claude kill <id> → 终止后台会话
│ └── 发送 SIGTERM → 等待 → SIGKILL

└── --bg / --background → 以后台模式启动
└── 立即返回会话 ID

SubagentStop Hook

子代理完成时触发 SubagentStop Hook,允许父代理或 Hook 系统检查子代理的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SubagentStop Hook 输入
{
event: 'SubagentStop',
data: {
agentId: string,
agentType: 'agent' | 'fork' | 'swarm_member' | 'task',
result: ToolResult,
turnCount: number,
stopReason: string,
}
}

// Hook 可以返回
{
blocking: string[], // 阻塞错误 → 子代理继续执行
preventContinuation: boolean, // 阻止子代理继续
}

Worktree 模式

Worktree 模式(feature: WORKTREE_MODE)允许代理在 Git worktree 中工作,实现真正的文件系统隔离:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Worktree 模式

├── EnterWorktreeTool
│ ├── git worktree add <path> <branch>
│ ├── 切换工作目录到 worktree
│ └── 所有后续文件操作在 worktree 中

├── 工作流程
│ ├── 主代理在主工作目录
│ ├── 子代理在 worktree 中
│ ├── 互不干扰的文件修改
│ └── 通过 git merge 合并结果

└── ExitWorktreeTool
├── 切换回主工作目录
├── 可选:合并 worktree 的修改
└── 可选:删除 worktree

Tmux 集成--worktree --tmux 快速路径在 cli.tsx 中处理——创建 worktree 并在新的 tmux 窗口中启动 Claude Code,实现真正的并行开发体验。

内置代理类型

源码中定义了多种内置代理类型(src/tools/AgentTool/built-in/),每种针对特定任务优化:

代理类型 用途 工具集 特殊行为
explore 代码探索和理解 Read, Glob, Grep, Bash(只读) 只读模式,不修改文件
plan 任务规划和分析 Read, Glob, Grep 生成结构化计划,不执行
verification 代码验证和测试 Read, Bash, Grep 运行测试、lint、类型检查
general-purpose 通用子任务 继承父代理工具集 默认类型,最灵活
claude-code-guide Claude Code 使用指导 Read 回答关于 Claude Code 本身的问题

每种内置代理都有专门的 system prompt 补充,指导模型在特定角色下的行为。例如 verification 代理的 prompt 强调"运行所有相关测试"、“检查类型错误”、“不要修复问题,只报告发现”。explore 代理的 prompt 强调"广泛搜索"、“理解代码结构”、“不要修改任何文件”。

代理选择逻辑:主代理在调用 AgentTool 时,通过 agentType 参数指定子代理类型。如果未指定,默认使用 general-purpose。模型会根据任务性质自主选择合适的代理类型——例如用户要求"帮我理解这个模块的架构"时,模型倾向于选择 explore 类型。

协调器模式(Coordinator Mode)

src/coordinator/coordinatorMode.ts(18.93KB)实现了协调器模式——一种更高级的多代理编排方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
协调器模式工作流程

├── 角色
│ ├── Coordinator(协调器):主代理,负责任务分配和结果汇总
│ └── Workers(工作者):子代理,负责执行具体任务

├── 工作流程
│ ├── 1. 用户提交任务给 Coordinator
│ ├── 2. Coordinator 分析任务,拆分为子任务
│ ├── 3. Coordinator 为每个子任务创建 Worker
│ │ ├── 选择合适的代理类型
│ │ ├── 分配工具权限
│ │ └── 提供任务描述和上下文
│ ├── 4. Workers 并行执行子任务
│ │ ├── 通过 SendMessageTool 向 Coordinator 报告进度
│ │ └── 完成后返回结果
│ ├── 5. Coordinator 汇总所有 Worker 结果
│ └── 6. Coordinator 生成最终响应给用户

├── Idle Cycle(空闲循环)
│ ├── Coordinator 在等待 Workers 时进入空闲循环
│ ├── 定期检查 Workers 的状态
│ ├── 处理 Workers 的权限请求
│ └── 响应用户的中断信号

├── Auto-Claim(自动认领)
│ ├── 当新任务到达时,Coordinator 自动认领
│ ├── 无需用户手动分配
│ └── 基于任务优先级和 Worker 可用性决策

└── 权限代理
├── awaitAutomatedChecksBeforeDialog = true
├── Workers 的权限请求先经过 Coordinator 的自动化检查
│ ├── classifier 判断
│ └── hooks 检查
└── 只有未通过自动化检查的请求才提交给用户

远程代理任务

src/tasks/RemoteAgentTask/RemoteAgentTask.tsx(124.26KB)和 src/tasks/LocalAgentTask/LocalAgentTask.tsx(81.63KB)实现了远程和本地代理任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
代理任务类型

├── LocalAgentTask(本地代理任务)
│ ├── 在本地进程中执行
│ ├── 共享本地文件系统
│ ├── 可以使用所有本地工具
│ └── 适用于需要文件系统访问的任务

├── RemoteAgentTask(远程代理任务)
│ ├── 在远程环境中执行(BYOC、Self-Hosted Runner)
│ ├── 通过 API 通信
│ ├── 独立的文件系统和环境
│ ├── 适用于 CI/CD 集成和云端执行
│ └── 支持 GitHub Action 集成

└── InProcessTeammateTask(进程内队友任务)
├── 在同一进程中执行
├── 共享内存空间
├── 最低开销
└── 适用于轻量级协作

Swarm 模式详细

Swarm 模式(src/utils/swarm/)实现了 Lead Agent + Teammates 的协作模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Swarm 模式架构

├── Lead Agent(领导代理)
│ ├── 接收用户任务
│ ├── 分析任务并创建团队
│ ├── 分配角色和职责
│ ├── 监控团队进度
│ └── 汇总最终结果

├── Teammates(队友代理)
│ ├── 接收 Lead Agent 分配的子任务
│ ├── 独立执行(独立进程、独立上下文)
│ ├── 通过 SendMessageTool 与其他队友通信
│ ├── 通过 TaskBoard 共享任务状态
│ └── 完成后向 Lead Agent 报告

├── TaskBoard(任务板)
│ ├── 共享的任务状态存储
│ ├── 每个任务有状态(pending/in-progress/done/blocked)
│ ├── 队友可以认领未分配的任务
│ └── 支持任务依赖关系

├── Mailbox(邮箱)
│ ├── 每个代理有独立的消息邮箱
│ ├── SendMessageTool 将消息投递到目标邮箱
│ ├── 代理在循环中轮询自己的邮箱
│ └── 支持广播消息(发送给所有队友)

└── 生命周期
├── TeamCreateTool → 创建团队 + 生成代理进程
├── 执行阶段 → 代理并行工作 + 消息通信
├── 汇总阶段 → Lead Agent 收集所有结果
└── TeamDeleteTool → 终止所有进程 + 清理资源

代理间的权限传播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
权限传播规则

├── AgentTool 子代理
│ ├── 继承父代理的权限模式
│ ├── 继承父代理的规则集
│ └── 会话级权限决策不继承(每个子代理独立)

├── Fork 子代理
│ ├── 继承创建时的权限快照
│ ├── 运行期间的权限变更不同步
│ └── shouldAvoidPermissionPrompts 可设置

├── Swarm 成员
│ ├── 继承团队创建时的权限配置
│ ├── 协调器可以代理权限决策
│ └── shouldAvoidPermissionPrompts = true(默认)

└── Task
├── 继承创建时的权限配置
├── 后台执行时无法交互式确认
└── shouldAvoidPermissionPrompts = true

八、MCP 协议集成

Claude Code 实现了完整的 MCP(Model Context Protocol)客户端,支持五种传输层、OAuth 2.0 认证、会话管理和工具注册。同时,Claude Code 自身也可以作为 MCP 服务器暴露给其他客户端。

MCP 架构总览

五种传输层

传输层 实现 通信方式 适用场景
stdio StdioClientTransport 子进程 stdin/stdout 本地工具(最常用)
SSE SSEClientTransport HTTP Server-Sent Events 远程服务器
Streamable HTTP StreamableHTTPClientTransport HTTP 双向流 新一代远程服务器
Named Pipe 自定义实现 Windows Named Pipe Windows 本地 IPC
Unix Socket 自定义实现 Unix Domain Socket Unix 本地 IPC

传输层选择逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
传输层选择

├── 配置中指定 transport?
│ ├── "stdio" → StdioClientTransport
│ ├── "sse" → SSEClientTransport
│ └── "streamable-http" → StreamableHTTPClientTransport

├── 配置中有 command?
│ └── → StdioClientTransport(子进程模式)

├── 配置中有 url?
│ ├── 尝试 Streamable HTTP
│ ├── 失败 → 降级到 SSE
│ └── SSE 也失败 → 报错

└── 配置中有 pipeName / socketPath?
└── → Named Pipe / Unix Socket

MCP 配置来源

MCP 服务器配置从多个来源加载,按优先级合并:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
MCP 配置来源(优先级高 → 低)

├── 1. CLI 参数
│ └── --mcp-config <path>

├── 2. 项目级配置
│ └── .mcp.json(项目根目录)

├── 3. 用户级配置
│ └── ~/.claude/mcp_servers.json

├── 4. 设置文件中的 mcpServers
│ ├── settings.json(各层级)
│ └── managed settings(企业)

├── 5. claude.ai 代理服务器
│ └── fetchClaudeAIMcpConfigsIfEligible()
│ └── 需要 OAuth 认证 + 网络请求

└── 6. 企业策略过滤
└── filterMcpServersByPolicy()
└── 可以禁止特定服务器

配置格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "ghp_..."
}
},
"remote-api": {
"url": "https://api.example.com/mcp",
"transport": "streamable-http",
"headers": {
"Authorization": "Bearer ..."
}
}
}
}

连接生命周期

Session Expired 处理

MCP 协议中的 Session Expired 错误需要特殊处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Session Expired 恢复流程

├── 检测到 Session Expired 错误

├── 1. 关闭当前连接
│ └── 清理所有待处理的请求

├── 2. 重新建立连接
│ └── 使用相同的配置参数

├── 3. 重新初始化
│ ├── initialize() 协议握手
│ ├── 交换能力
│ └── 重新发现工具

├── 4. 重新注册工具
│ ├── 检查工具列表是否变化
│ ├── 新增工具 → 添加到工具池
│ ├── 移除工具 → 从工具池删除
│ └── 变更工具 → 更新 schema

└── 5. 重试失败的请求
└── 使用新的 session ID

工具注册与命名

MCP 工具在 Claude Code 中的命名遵循严格的约定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
MCP 工具命名约定

├── 格式: mcp__{serverName}__{toolName}
│ ├── 双下划线分隔
│ ├── serverName: 配置中的服务器名称
│ └── toolName: 服务器报告的工具名称

├── 示例
│ ├── mcp__github__create_issue
│ ├── mcp__filesystem__read_file
│ └── mcp__sentry__search_issues

├── 权限规则匹配
│ ├── 精确匹配: "mcp__github__create_issue"
│ ├── 服务器前缀: "mcp__github" → 匹配该服务器所有工具
│ └── 通配符: "mcp__*__read_*" → 匹配所有服务器的 read 工具

└── 名称冲突处理
├── 内置工具优先(uniqBy('name'))
└── MCP 工具之间:先注册的优先

MCPTool 包装器

src/tools/MCPTool/MCPTool.ts 将 MCP 工具包装为 Claude Code 的 Tool 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
MCPTool 包装逻辑

├── inputSchema
│ ├── 从 MCP 工具的 JSON Schema 转换为 Zod schema
│ └── 支持 $ref 解引用

├── call()
│ ├── 序列化输入为 JSON
│ ├── 调用 MCP tools/call
│ ├── 反序列化结果
│ └── 处理错误(超时、连接断开等)

├── prompt()
│ ├── 使用 MCP 工具的 description
│ ├── 添加服务器级别的指令(getMcpInstructionsSection)
│ └── 添加 _meta['anthropic/instructions'] 中的指令

├── isConcurrencySafe()
│ └── MCP 工具默认 true(假设服务器处理并发)

├── isReadOnly()
│ └── 从 _meta['anthropic/readOnly'] 读取

├── shouldDefer
│ └── 从 _meta['anthropic/shouldDefer'] 读取

├── alwaysLoad
│ └── 从 _meta['anthropic/alwaysLoad'] 读取

└── maxResultSizeChars
└── 100_000(MCP 工具统一限制)

MCP 资源系统

除了工具,MCP 还支持资源(Resources)和资源模板(Resource Templates):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
MCP 资源系统

├── ListMcpResourcesTool
│ ├── 列出服务器提供的所有资源
│ ├── 资源 URI + 描述
│ └── 支持分页

├── ReadMcpResourceTool
│ ├── 读取特定资源的内容
│ ├── 支持 URI 模板参数
│ └── 结果作为工具结果返回

├── 资源预取
│ ├── prefetchAllMcpResources()
│ ├── 在启动时预取所有资源列表
│ └── 缓存资源元数据

└── 资源类型
├── text/* → 文本内容
├── application/json → JSON 数据
└── image/* → Base64 编码图片

OAuth 2.0 认证流程

远程 MCP 服务器支持 OAuth 2.0 认证:

PKCE(Proof Key for Code Exchange):所有 OAuth 流程都使用 PKCE 扩展,防止授权码拦截攻击。code_verifier 是一个随机字符串,code_challenge 是其 SHA-256 哈希。

Token 存储:OAuth token 存储在 ~/.claude/mcp_tokens.json 中,按服务器 URL 索引。Token 包含 access_tokenrefresh_tokenexpires_at 等字段。

Elicitation 机制

MCP 服务器可以通过 Elicitation 请求用户输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Elicitation 流程

├── MCP 服务器发送 elicitation 请求
│ ├── 请求类型(text、select、confirm)
│ ├── 提示信息
│ └── 可选的默认值

├── Claude Code 处理
│ ├── 检查 Elicitation Hook
│ │ └── Hook 可以自动回答(无需用户交互)
│ ├── 无 Hook → 显示 UI 提示
│ │ ├── Terminal UI
│ │ ├── Bridge UI
│ │ └── Channel UI
│ └── 收集用户输入

└── 返回结果给 MCP 服务器
└── 用户的输入值

claude.ai MCP 代理

fetchClaudeAIMcpConfigsIfEligible()claude.ai 获取 MCP 服务器配置——这些是 Anthropic 托管的 MCP 代理服务器,通过 SSE 连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
claude.ai MCP 代理

├── 资格检查
│ ├── 需要 OAuth 认证(有用户上下文)
│ ├── 需要 feature gate 启用
│ └── CLAUDE_CODE_SIMPLE !== '1'(bare 模式禁用)

├── 配置获取
│ ├── GET /api/mcp-configs
│ ├── 返回可用的代理服务器列表
│ └── 每个服务器包含 URL + 认证信息

├── 连接
│ ├── 使用 SSE 传输层
│ ├── 附加 OAuth token 作为认证
│ └── 连接超时:6-14 秒/服务器

└── 性能影响
├── 每个代理服务器 6-14 秒连接时间
├── bare 模式跳过(节省启动时间)
└── 连接失败不阻塞启动

MCP 服务器模式

Claude Code 自身也可以作为 MCP 服务器运行(src/entrypoints/mcp.ts):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Claude Code 作为 MCP 服务器

├── 启动方式
│ └── claude mcp serve

├── 暴露的工具
│ ├── 所有内置工具
│ ├── 所有已连接的 MCP 工具(代理转发)
│ └── 自定义工具(通过配置)

├── 传输层
│ ├── stdio(默认)
│ └── SSE(通过 --port 参数)

└── 使用场景
├── Claude Desktop 集成
├── 其他 MCP 客户端调用 Claude Code 的能力
└── 工具链组合(Claude Code 作为中间层)

Chrome 扩展 MCP

--claude-in-chrome-mcp 启动 Chrome 扩展专用的 MCP 服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Chrome MCP 服务器

├── 通信方式
│ └── Chrome Native Messaging Host
│ └── --chrome-native-host 参数

├── 功能
│ ├── 浏览器页面内容读取
│ ├── 浏览器操作执行
│ └── 与 Claude Code 主进程通信

└── 安全
├── 只允许来自 Chrome 扩展的连接
└── 受 Chrome 扩展权限模型保护

MCP 指令注入

getMcpInstructionsSection() 将 MCP 服务器的指令注入到 system prompt 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// MCP 指令来源
// 1. 服务器级别指令(initialize 响应中的 instructions 字段)
// 2. 工具级别指令(_meta['anthropic/instructions'])
// 3. 用户配置的指令(settings.json 中的 mcpServerInstructions)

// 注入格式
`## MCP Server Instructions

### ${serverName}
${serverInstructions}

### Tool: ${toolName}
${toolInstructions}
`

这些指令位于 system prompt 的动态区域,不影响静态区域的 Prompt Cache。

XAA 跨应用访问

src/services/mcp/xaa.ts(18.36KB)和 src/services/mcp/xaaIdpLogin.ts(16.37KB)实现了跨应用访问(Cross-Application Access)机制——允许 Claude Code 代表用户访问其他 Anthropic 应用的 MCP 服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
XAA 跨应用访问流程

├── 场景
│ ├── Claude Code 需要访问 claude.ai 的 MCP 代理服务器
│ ├── 这些服务器需要验证用户身份
│ └── 用户已经在 Claude Code 中通过 OAuth 认证

├── 认证流程
│ ├── 1. 检查本地是否有有效的 XAA token
│ ├── 2. 如果没有 → 发起 XAA IDP 登录
│ │ ├── 使用 Claude Code 的 OAuth token 作为身份证明
│ │ ├── 向 Anthropic IDP 请求 XAA token
│ │ └── XAA token 包含跨应用访问权限
│ ├── 3. 使用 XAA token 连接 MCP 代理服务器
│ └── 4. 缓存 XAA token(有效期内复用)

├── 安全模型
│ ├── XAA token 的权限范围受限于用户的 OAuth scope
│ ├── 每个 MCP 服务器有独立的 XAA token
│ ├── Token 过期后自动刷新
│ └── 用户可以随时撤销 XAA 授权

└── 与 McpAuthTool 的关系
├── McpAuthTool 处理标准的 MCP OAuth 认证
└── XAA 处理 Anthropic 生态内的跨应用认证

McpAuthTool

src/tools/McpAuthTool/McpAuthTool.ts(7.90KB)提供了 MCP 服务器认证的交互式工具——当 MCP 服务器需要认证但自动认证失败时,模型可以调用此工具引导用户完成认证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
McpAuthTool 工作流程

├── 触发条件
│ ├── MCP 服务器返回 401/403 错误
│ ├── 或连接时要求认证但无有效 token
│ └── 自动认证(XAA、缓存 token)失败

├── 执行流程
│ ├── 1. 识别服务器的认证方式
│ │ ├── OAuth 2.0 → 启动 OAuth 流程
│ │ ├── API Key → 提示用户输入
│ │ └── Bearer Token → 提示用户输入
│ ├── 2. 引导用户完成认证
│ │ ├── 打开浏览器(OAuth)
│ │ ├── 或显示输入提示(API Key)
│ │ └── 等待用户完成
│ ├── 3. 存储认证信息
│ │ └── ~/.claude/mcp_tokens.json
│ └── 4. 重新连接 MCP 服务器

└── 与权限系统的交互
├── McpAuthTool 需要用户确认(ask 权限)
└── 防止恶意 MCP 服务器自动触发认证流程

MCP 连接的健康检查

MCP 连接管理器(src/services/mcp/MCPConnectionManager.tsx,8.06KB)实现了连接的健康检查和自动恢复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
连接健康检查

├── 心跳检测
│ ├── 定期发送 ping 请求
│ ├── 超时未响应 → 标记为不健康
│ └── 连续 N 次不健康 → 触发重连

├── 工具列表刷新
│ ├── 定期调用 tools/list
│ ├── 检测工具变化(新增/移除/修改)
│ ├── 变化 → 更新 assembleToolPool()
│ └── 通知 UI 更新工具列表

├── 错误恢复策略
│ ├── 网络错误 → 指数退避重试(1s, 2s, 4s, 8s, 16s)
│ ├── 服务器崩溃 → 重启子进程(stdio 传输层)
│ ├── Session Expired → 重新初始化
│ └── 认证过期 → 刷新 token 后重连

└── 连接池管理
├── 每个 MCP 服务器一个连接
├── 连接复用(同一服务器的多个工具共享连接)
├── 空闲连接超时关闭(节省资源)
└── 最大连接数限制(防止资源耗尽)

九、遥测、安全与隐藏功能

Claude Code 内置了完整的遥测系统、A/B 测试框架、PII 隔离机制,以及多个未公开的隐藏功能。这些系统共同支撑了产品的数据驱动迭代和安全合规。

双层遥测管道

遥测系统分为两层——第一方事件日志(1P Event Logging)和第三方分析(Sentry/Statsig),各自有不同的数据范围和隐私级别。

1P Event Logging

src/services/firstPartyEventLogger.ts 实现了第一方事件日志系统:

初始化流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
initialize1PEventLogging()

├── 延迟加载 OpenTelemetry SDK
│ └── Promise.all([import('sdk-logs'), import('growthbook')])

├── 创建 LoggerProvider
│ ├── BatchLogRecordExporter
│ │ ├── 批量大小: 50 条
│ │ ├── 发送间隔: 5 秒
│ │ └── 队列上限: 1000 条
│ └── 目标端点: Anthropic 遥测 API

├── 注册 GrowthBook 刷新回调
│ └── onGrowthBookRefresh() → reinitialize1PEventLoggingIfConfigChanged()
│ └── 配置变更时重新初始化(如端点 URL 变更)

└── 注册清理函数
└── registerCleanup(() => loggerProvider.shutdown())

事件类型

事件 触发时机 包含数据
tengu_timer 启动完成 启动耗时、各阶段时间戳
tool_use 工具调用 工具名称、耗时、成功/失败
api_call API 请求 模型、token 数、延迟
permission_decision 权限决策 工具、决策、来源
compact 压缩触发 压缩前后 token 数
session_end 会话结束 总轮次、总 token、总耗时
error 错误发生 错误类型、堆栈(脱敏)

PII 隔离:PROTO* 字段

遥测数据中的 PII(个人可识别信息)通过 _PROTO_* 前缀字段隔离:

1
2
3
4
5
6
7
8
9
10
11
12
// 遥测事件示例
{
event: 'tool_use',
tool_name: 'BashTool', // 非 PII
duration_ms: 1234, // 非 PII
success: true, // 非 PII

// PII 字段——存储在隔离的数据管道中
_PROTO_command: 'git commit -m "fix: user login"', // 可能包含用户信息
_PROTO_file_path: '/home/user/project/auth.ts', // 包含用户路径
_PROTO_error_message: 'Connection refused to db.internal.company.com',
}

隔离机制

  • _PROTO_* 字段在传输前被分离到独立的数据流
  • 非 PII 字段进入标准分析管道(可聚合、可查询)
  • PII 字段进入受限管道(更短的保留期、更严格的访问控制)
  • 客户端可以通过设置完全禁用 PII 字段的发送

GrowthBook A/B 测试

src/services/growthbook.ts 集成了 GrowthBook SDK,用于 feature flag 和 A/B 测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GrowthBook 集成

├── 初始化
│ ├── 加载 GrowthBook SDK
│ ├── 设置用户属性(user ID、组织、计划等)
│ ├── 获取 feature flag 配置
│ └── 注册刷新回调

├── Feature Flag 评估
│ ├── feature('FLAG_NAME') → boolean
│ ├── 编译时 DCE(Dead Code Elimination)
│ │ └── 未启用的 feature 代码在构建时被移除
│ └── 运行时评估(基于用户属性)

├── A/B 测试
│ ├── 基于用户 ID 的确定性分桶
│ ├── 支持多变体实验
│ └── 曝光事件自动记录

└── 刷新机制
├── 定期轮询(默认 5 分钟)
├── 配置变更 → 触发回调
└── 回调可以重新初始化依赖系统

Feature Flag 的编译时门控

1
2
3
4
5
6
// feature() 函数在构建时被评估
// 如果 flag 未启用,整个代码块被 DCE 移除
if (feature('AGENT_SWARMS')) {
// 这段代码只在 AGENT_SWARMS 启用时存在于 bundle 中
tools.push(TeamCreateTool, TeamDeleteTool)
}

已知的 Feature Flag 清单(部分):

Flag 功能 状态
AGENT_SWARMS 多代理团队协作 实验性
BG_SESSIONS 后台会话管理 实验性
BRIDGE_MODE Bridge 远程控制 实验性
CONTEXT_COLLAPSE 上下文折叠 渐进推出
HISTORY_SNIP 历史裁剪 渐进推出
TOOL_SEARCH 工具搜索 渐进推出
TASKS 后台任务系统 实验性
WORKTREE_MODE Git Worktree 模式 实验性
DAEMON 守护进程模式 实验性
KAIROS 定时任务/推送通知 实验性
TEMPLATES 模板任务 实验性
BYOC_ENVIRONMENT_RUNNER BYOC 环境运行器 实验性
SELF_HOSTED_RUNNER 自托管运行器 实验性
AUTO_MEMORY 自动记忆提取 渐进推出
BASH_CLASSIFIER Bash 命令分类器 渐进推出
TRANSCRIPT_CLASSIFIER 对话分类器 渐进推出
ABLATION_BASELINE L0 消融基线 Ant-only
CHICAGO_MCP Computer Use MCP 实验性

Sentry 错误追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Sentry 集成

├── 初始化
│ ├── initializeTelemetryAfterTrust()
│ │ └── 必须在 Trust Dialog 之后(用户同意遥测)
│ ├── 配置 DSN、环境、版本
│ └── 设置采样率

├── 错误捕获
│ ├── 未捕获异常 → 自动上报
│ ├── 未处理的 Promise rejection → 自动上报
│ ├── 手动 captureException() → 主动上报
│ └── 面包屑(breadcrumbs)→ 上下文追踪

├── 数据脱敏
│ ├── 文件路径 → 相对路径
│ ├── 用户名 → 哈希
│ ├── API Key → 移除
│ └── 自定义 beforeSend 过滤器

└── 性能监控
├── Transaction 追踪
├── API 调用延迟
└── 工具执行耗时

Undercover 模式

Undercover 模式是一个隐藏功能——当启用时,Claude Code 伪装为其他 AI 编码助手:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Undercover 模式

├── 触发方式
│ └── 环境变量或内部配置(未公开)

├── 行为变化
│ ├── System Prompt 中移除 "Claude" 相关引用
│ ├── 替换为通用的 "AI Assistant" 描述
│ ├── 移除 Anthropic 特定的指令
│ └── 调整语气和风格

├── 用途
│ ├── 竞品分析和基准测试
│ ├── 避免模型在输出中自我引用
│ └── 第三方集成中的白标需求

└── 实现
├── getSimpleIntroSection() 中的条件分支
├── 检查 undercover 配置
└── 替换身份声明文本

CYBER_RISK_INSTRUCTION

System Prompt 中包含一段安全指令 CYBER_RISK_INSTRUCTION,用于防止模型被利用进行恶意操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CYBER_RISK_INSTRUCTION 内容(概要)

├── 禁止生成恶意代码
│ ├── 恶意软件、病毒、蠕虫
│ ├── 漏洞利用代码
│ └── 社会工程攻击脚本

├── 禁止协助攻击
│ ├── 网络入侵
│ ├── 数据窃取
│ └── 权限提升

├── 安全编码指导
│ ├── 输入验证
│ ├── 输出编码
│ └── 安全默认值

└── 注入位置
└── getSimpleIntroSection() 的身份声明之后
└── 位于 system prompt 的最前面(最高优先级)

性能追踪系统

profileCheckpoint()

1
2
3
4
5
6
7
8
9
// 在关键节点记录时间戳
profileCheckpoint('cli_entry')
profileCheckpoint('init_function_start')
profileCheckpoint('init_configs_enabled')
// ... 更多检查点
profileCheckpoint('cli_after_main_complete')

// 所有检查点在 tengu_timer 事件中上报
// 用于分析启动性能瓶颈

Event Loop Stall Detector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
事件循环阻塞检测

├── 原理
│ ├── 注册一个 setInterval(间隔 500ms)
│ ├── 记录每次回调的实际时间
│ └── 如果实际间隔 > 预期间隔 + 阈值 → 记录阻塞

├── 阈值
│ └── 500ms(主线程被阻塞超过 500ms 时触发)

├── 记录内容
│ ├── 阻塞持续时间
│ ├── 阻塞发生时的调用栈(如果可用)
│ └── 当前正在执行的操作

└── 用途
├── 诊断启动卡顿
├── 识别同步 I/O 操作
└── 发现 CPU 密集型计算

Prompt Cache 四层优化

Prompt Cache 是 Claude Code 最重要的性能优化之一——通过缓存 system prompt 和工具 schema,避免每次 API 调用都重新处理这些内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Prompt Cache 四层优化

├── Layer 1: System Prompt 静态/动态分区
│ ├── 静态部分(SYSTEM_PROMPT_DYNAMIC_BOUNDARY 之前)
│ │ └── 跨用户、跨会话可缓存
│ ├── 动态部分(BOUNDARY 之后)
│ │ └── 会话级缓存
│ └── 分区保证静态部分的 cache key 稳定

├── Layer 2: 工具 Schema 排序稳定性
│ ├── 内置工具按名称排序(前缀)
│ ├── MCP 工具按名称排序(后缀)
│ └── 增减 MCP 工具不影响内置工具的位置

├── Layer 3: cache_control 标记
│ ├── ephemeral breakpoint 标记
│ ├── 告诉 API 哪些内容可以缓存
│ └── normalizeMessages() 中注入

└── Layer 4: 子代理 Prompt Cache 共享
├── AgentTool 子代理继承主代理的静态 prompt
├── 子代理首次调用命中主代理的缓存
└── 节省约 15% 的 token 成本

内存管理

Claude Code 作为长时间运行的 Node.js 进程,内存管理至关重要:

已知的内存泄漏场景

场景 原因 缓解措施
消息历史无限增长 长会话积累大量消息 autoCompact 定期压缩
工具结果未释放 大文件内容保留在内存 applyToolResultBudget 持久化到磁盘
MCP 连接泄漏 服务器崩溃后连接未清理 Session Expired 检测 + 重连
Sentry 面包屑 面包屑队列无限增长 设置 maxBreadcrumbs 上限
GrowthBook 缓存 Feature flag 缓存未过期 定期刷新 + 内存上限
子进程句柄 Fork 子进程未正确终止 registerCleanup + SIGKILL 兜底

CCR 容器环境CLAUDE_CODE_REMOTE=true 时设置 --max-old-space-size=8192(8GB),防止容器 OOM。

优雅退出

setupGracefulShutdown() 注册了多层退出处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
优雅退出流程

├── 触发信号
│ ├── SIGINT(Ctrl+C)
│ ├── SIGTERM(进程管理器)
│ └── SIGHUP(终端关闭)

├── 退出序列
│ ├── 1. 执行 SessionEnd Hook
│ ├── 2. 停止所有后台任务
│ ├── 3. 关闭 MCP 连接
│ ├── 4. 关闭 LSP 服务器
│ ├── 5. 清理 Swarm 团队
│ ├── 6. 刷新遥测缓冲区
│ ├── 7. 关闭 Sentry
│ └── 8. 清理临时文件(Scratchpad)

├── 超时保护
│ ├── 每个清理步骤有独立超时
│ ├── 总超时 10 秒
│ └── 超时后强制退出(process.exit(1))

└── registerCleanup() 注册的清理函数
├── shutdownLspServerManager
├── cleanupSessionTeams
├── loggerProvider.shutdown()
└── 自定义清理函数

安全审计日志

所有安全相关的操作都记录审计日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
审计日志记录的事件

├── 权限决策
│ ├── 工具名称、输入摘要
│ ├── 决策结果(allow/deny/ask)
│ ├── 决策来源(rule/classifier/user/hook)
│ └── 规则匹配详情

├── Hook 执行
│ ├── Hook 类型(command/prompt/http)
│ ├── Hook 来源(managed/user/project)
│ ├── 执行结果
│ └── 执行耗时

├── 认证事件
│ ├── OAuth 登录/刷新/失败
│ ├── API Key 使用
│ └── Token 过期

├── 配置变更
│ ├── 设置文件修改
│ ├── 权限规则变更
│ └── MCP 服务器增减

└── 存储位置
├── 1P Event Logging(远程)
├── 本地日志文件(~/.claude/logs/)
└── Sentry(错误相关)

企业管控能力

企业管理员通过 managed settings 和策略控制 Claude Code 的行为:

管控项 配置路径 说明
工具权限 managed.alwaysDeny 禁止特定工具
MCP 服务器 managed.mcpServers 强制/禁止特定服务器
Hook managed.hooks 强制执行的 Hook
模型 managed.model 强制使用特定模型
遥测 managed.telemetry 控制遥测数据范围
远程控制 managed.allow_remote_control 允许/禁止 Bridge 模式
权限模式 managed.permissionMode 强制权限模式
网络 managed.proxy 强制代理配置

策略限制initializePolicyLimitsLoadingPromise() 在启动时加载企业策略限制——这些限制不能被用户或项目设置覆盖。

隐藏的调试功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
隐藏调试功能

├── --dump-system-prompt
│ ├── 输出完整的渲染后 system prompt
│ ├── Ant-only(需要内部认证)
│ └── 用于调试 prompt 组装问题

├── CLAUDE_CODE_DEBUG=1
│ ├── 启用详细日志输出
│ ├── 显示 API 请求/响应详情
│ └── 显示工具执行详情

├── CLAUDE_CODE_ABLATION_BASELINE
│ ├── L0 消融基线模式
│ ├── 禁用所有高级功能
│ ├── 用于 A/B 测试基线对比
│ └── Ant-only

├── profileCheckpoint 输出
│ ├── 启动性能详细时间线
│ └── 通过遥测事件上报

└── Event Loop Stall 日志
├── 主线程阻塞检测
└── 输出到本地日志

Datadog 集成

src/services/analytics/datadog.ts(9.19KB)实现了 Datadog APM 集成,提供更细粒度的性能监控:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Datadog 集成

├── 指标类型
│ ├── 计数器(Counter)
│ │ ├── api_calls_total // API 调用总数
│ │ ├── tool_executions_total // 工具执行总数
│ │ ├── permission_decisions // 权限决策次数
│ │ └── compact_triggers // 压缩触发次数
│ │
│ ├── 直方图(Histogram)
│ │ ├── api_latency_ms // API 延迟分布
│ │ ├── tool_execution_ms // 工具执行时间分布
│ │ ├── startup_time_ms // 启动时间分布
│ │ └── compact_duration_ms // 压缩耗时分布
│ │
│ └── 仪表盘(Gauge)
│ ├── context_tokens_used // 当前上下文 token 使用量
│ ├── active_mcp_connections // 活跃 MCP 连接数
│ └── memory_entries_count // 记忆条目数

├── 标签(Tags)
│ ├── model: claude-opus-4-6
│ ├── entrypoint: cli / sdk / mcp
│ ├── user_type: ant / external
│ └── permission_mode: default / auto / bypass

└── 与 1P Event Logging 的关系
├── 1P 记录产品事件(用户行为分析)
└── Datadog 记录性能指标(运维监控)

环境指纹收集

src/services/analytics/metadata.ts(32.80KB)收集详细的环境元数据,用于遥测分析和问题诊断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
环境指纹收集的字段

├── 平台信息
│ ├── os: 'darwin' | 'linux' | 'win32'
│ ├── arch: 'x64' | 'arm64'
│ ├── platform_version: '14.2.1'(macOS 版本)
│ ├── kernel_version: '6.5.0-44-generic'(Linux 内核)
│ └── is_wsl: boolean(Windows Subsystem for Linux)

├── 终端信息
│ ├── terminal: 'iTerm2' | 'Terminal.app' | 'Windows Terminal' | ...
│ ├── shell: 'zsh' | 'bash' | 'fish' | 'powershell'
│ ├── term_program: $TERM_PROGRAM
│ ├── term_colors: $COLORTERM
│ └── columns / rows(终端尺寸)

├── IDE 信息
│ ├── ide: 'vscode' | 'jetbrains' | 'cursor' | 'windsurf' | ...
│ ├── ide_version: '1.85.0'
│ └── is_remote: boolean(SSH / Remote Desktop)

├── CI/CD 信息
│ ├── is_ci: boolean
│ ├── ci_provider: 'github-actions' | 'gitlab-ci' | 'jenkins' | ...
│ ├── ci_job_id: string
│ └── ci_branch: string

├── 运行时信息
│ ├── node_version: '20.10.0'
│ ├── bun_version: '1.1.0'(如果使用 Bun)
│ ├── claude_code_version: '2.1.88'
│ └── npm_version: '10.2.3'

├── 项目信息
│ ├── git_remote_url_hash: SHA-256(隐私保护)
│ ├── file_count: 1234(项目文件数)
│ ├── has_package_json: boolean
│ ├── has_tsconfig: boolean
│ └── primary_language: 'typescript' | 'python' | ...

└── 用户信息
├── user_id_hash: SHA-256(隐私保护)
├── organization_id: string(OAuth 用户)
├── plan: 'free' | 'pro' | 'team' | 'enterprise'
└── account_age_days: number

隐私保护:所有可能包含 PII 的字段都经过哈希处理——git_remote_url_hash 使用 SHA-256 哈希仓库 URL,user_id_hash 哈希用户 ID。这允许聚合分析(“有多少不同的仓库使用了 Claude Code”)而不暴露具体的仓库 URL。

模型代号体系

源码中使用动物代号指代不同的模型系列:

代号 模型 说明
Capybara Claude Sonnet 系列 主力模型,平衡性能和成本
Tengu Claude Opus 系列 最强模型,用于复杂任务
Fennec Claude Haiku 系列 轻量模型,用于分类器和摘要
Numbat 下一代模型 开发中,源码中有占位符

模型 ID 映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const MODEL_IDS = {
opus: 'claude-opus-4-6',
sonnet: 'claude-sonnet-4-6',
haiku: 'claude-haiku-4-5-20251001',
}

const FRONTIER_MODEL_NAME = 'Claude Opus 4.6'

// 知识截止日期
function getKnowledgeCutoff(model: string): string {
if (model.includes('opus-4-6')) return 'May 2025'
if (model.includes('sonnet-4-6')) return 'August 2025'
if (model.includes('haiku-4-5')) return 'Early 2025'
return 'Early 2025'
}

@[MODEL LAUNCH] 标记:源码中有 @[MODEL LAUNCH] 注释标记需要在新模型发布时更新的位置——包括模型 ID、知识截止日期、默认参数等。

模型迁移脚本

src/migrations/ 目录包含模型迁移脚本,用于在模型更新时自动迁移用户配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
模型迁移脚本

├── 迁移场景
│ ├── 模型 ID 变更(如 claude-3-opus → claude-opus-4)
│ ├── 默认模型切换(如 Sonnet 4.5 → Sonnet 4.6)
│ ├── 模型参数变更(如 max_tokens 默认值变化)
│ └── 模型能力变更(如新增 thinking 支持)

├── 迁移流程
│ ├── 1. 检查用户配置中的模型设置
│ ├── 2. 如果使用了旧模型 ID → 自动替换为新 ID
│ ├── 3. 如果使用了已废弃的参数 → 迁移到新参数
│ ├── 4. 记录迁移日志
│ └── 5. 通知用户(如果有破坏性变更)

├── 已知迁移
│ ├── Fennec → Haiku 4.5(模型代号到正式名称)
│ ├── Sonnet 4.5 → Sonnet 4.6(默认模型升级)
│ └── Opus 4.5 → Opus 4.6(默认模型升级)

└── 迁移版本追踪
├── ~/.claude/migration_version
├── 记录已执行的迁移版本
└── 防止重复执行

远程托管设置

src/services/remoteManagedSettings/index.ts(21.04KB)实现了远程托管设置——企业管理员可以通过 API 远程控制 Claude Code 的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
远程托管设置

├── 加载流程
│ ├── 启动时发起 API 请求获取远程设置
│ ├── 每小时轮询更新
│ ├── 设置缓存在本地(离线时使用缓存)
│ └── 设置变更触发 settingsChangeDetector

├── 可控制的设置
│ ├── 工具权限(alwaysDeny、alwaysAsk)
│ ├── MCP 服务器白名单/黑名单
│ ├── 模型限制(强制使用特定模型)
│ ├── 功能开关(禁用特定功能)
│ └── 安全策略(强制沙箱、禁止 bypass 模式)

├── Killswitch 机制
│ ├── 远程设置可以包含 killswitch 标志
│ ├── 激活时显示阻塞对话框
│ ├── 用户无法绕过(必须等待管理员解除)
│ └── 用于紧急安全事件响应

└── 安全检查
├── securityCheck.tsx(10.34KB)
├── 验证远程设置的签名
├── 防止中间人篡改设置
└── 设置过期检测(过期的设置不生效)

Beta 功能管理

src/utils/betas.ts(15.74KB)管理 API 级别的 beta 功能:

1
2
3
4
5
6
7
8
9
10
11
12
// 已知的 Beta 功能
const BETAS = {
'task-budgets-2026-03-13': true, // Task Budget API
'prompt-caching-2024-07-31': true, // Prompt Cache
'max-tokens-3-5-sonnet-2024-07-15': true, // 扩展 max_tokens
'output-128k-2025-02-19': true, // 128K 输出
'interleaved-thinking-2025-05-14': true, // 交错思考
'token-efficient-tools-2025-02-19': true, // Token 高效工具
}

// Beta 功能通过 API 请求头传递
// x-anthropic-beta: task-budgets-2026-03-13,prompt-caching-2024-07-31,...

未来路线图线索

源码中包含多个未来功能的线索:

线索 来源 推测
KAIROS Feature flags、system prompt 完全自主代理模式(tick 心跳、推送通知、PR 订阅)
Numbat 模型代号映射 下一代模型(可能是 Claude 5 系列)
语音模式 useVoiceIntegration.tsx(97.79KB) Push-to-talk 语音输入,WebSocket STT
WebBrowserTool Feature flag、工具注册 浏览器自动化(代号 bagel)
Buddy 系统 源码中的类型定义 虚拟宠物伙伴(18 物种、5 稀有度)
Opus 4.7 / Sonnet 4.8 @[MODEL LAUNCH] 标记 下一代模型版本
Plugin 系统 src/plugins/(多文件) 第三方插件生态
Skills 系统 src/skills/(多文件) 可复用的技能模板