Claude Code 源码分析笔记
Claude Code 源码分析
对照 nanobot 逆向理解 Claude Code Agent 架构。
目录
- Ch 00 - 全局流程图
- Ch 01 - 项目概览
- Ch 02 - 技术栈
- Ch 03 - 目录结构
- Ch 04 - 启动流程
- Ch 05 - LLM 流式调用细节
- Ch 06 - 工具调用系统
- Ch 07 - 流式执行引擎(默认关闭)
- Ch 08 - Thinking 推理模式
- Ch 09 - Task 系统
- Ch 10 - Skill 系统
- Ch 11 - Compact:对话压缩
- Ch 12 - 记忆系统
- Ch 13 - 上下文管理
- Ch 14 - Hooks 扩展
- Ch 15 - 沙盒与权限系统
- Ch 16 - MAS 转向 Harness Engineering
- Ch 17 - MCP 集成
- Ch 18 - 总结:Claude Code 架构全景
Ch 00 - 全局流程图
方括号为章节引用:Ch04 · Ch05 · Ch06 · Ch07 · Ch08 · Ch09 · Ch10 · Ch11 · Ch14
1 | ① [Ch04] main.tsx 启动 |
Ch 01 - 项目概览
| nanobot | Claude Code | |
|---|---|---|
| 语言 | Python | TypeScript (Bun) |
| 规模 | 小型 | ~50万行,生产级 |
| 工具数 | ~10 | 40+ 内置 + 动态 MCP |
| 入口 | loop.py |
src/query.ts(1732行) |
| UI | 无 | React+Ink 终端(前端,跳过) |
定位:terminal-native agentic system,核心是 Agent Loop 直接操作本地环境。
Agent 核心文件
| 文件 | 职责 |
|---|---|
src/query.ts |
Agent Loop 核心 |
src/QueryEngine.ts |
会话容器,包裹 query() |
src/Tool.ts |
Tool 类型定义 |
src/tools.ts |
工具注册 & 池化 |
src/services/tools/toolExecution.ts |
权限检查 + hooks + 执行 |
src/services/tools/toolOrchestration.ts |
并发/串行调度 |
src/utils/tasks.ts |
Task 系统 |
src/services/compact/ |
对话压缩 |
src/utils/hooks.ts |
Hooks 系统 |
src/utils/thinking.ts |
Thinking 模式 |
UI 层只消费 query.ts 的流式事件,核心逻辑全在 query.ts。
关键文件速查
1 | Agent Loop → src/query.ts + src/QueryEngine.ts |
Ch 02 - 技术栈
Bun:比 Node.js 启动快 ~3x,内置打包器,一套工具链搞定运行/打包/测试。代价是 Windows 支持弱、生态兼容性差一些。
Feature Flag(bun:bundle):打包时注入 flag 值,false 的分支物理删除。目的是同一份代码打出不同版本(内部/灰度/正式),不用维护多个分支。反编译版里 feature() 永远返回 false,实验功能全关。
前端:React + Ink,终端里跑 React 组件。REPL.tsx 调用 QueryEngine,消费流式事件刷新界面。Agent 逻辑与 UI 完全解耦。
Ch 03 - 目录结构
1 | src/ |
tools/ 结构:每个工具一个独立目录,tools.ts 统一注册。工具列表排序固定,保证 prompt cache 命中率稳定。
src/tools.ts 三个关键函数:
getAllBaseTools()- 返回所有内置工具getTools(context)- 按权限/feature flag 过滤assembleToolPool(context, mcpTools)- 内置 + MCP 合并,去重排序
Ch 04 - 启动流程
并行预取:main.tsx 顶部利用模块加载副作用,在其他 import 还在执行时就并行触发:MDM 企业策略、Keychain(API Key / OAuth)、GrowthBook feature flags。串行需 135ms+,并行只要 ~40ms。
命令分发:Commander.js 解析参数后,所有路径最终都汇入 launchRepl(),差别只是传入的初始消息和配置不同。
1 | main.tsx |
CoordinatorMode:src/coordinator/ 是多 Agent 团队协作模式,在启动时通过 feature flag 加载。默认双重关闭:① 构建时 feature('COORDINATOR_MODE') = false,代码不进二进制;② 即使代码存在,还需环境变量 CLAUDE_CODE_COORDINATOR_MODE=true 才能激活。对外版本根本进不了第一层。
多 Agent 两种方式对比:
| AgentTool | CoordinatorMode | |
|---|---|---|
| 默认状态 | 开启 | 关闭(实验性,内部专用) |
| 触发 | LLM 自己决定调用 | 专职 Coordinator 主导 |
| 定位 | 普通多 Agent | 结构化团队协作,AgentTool 进阶版 |
日常多 Agent 走 AgentTool,CoordinatorMode 是内部实验功能,对外不可用。
Ch 05 - LLM 流式调用细节
Agent:QueryEngine | queryLoop | State
没有 Agent 类,但是概念存在queryLoop()。运行时只有三层:
| 层 | 实体 | 跨越范围 |
|---|---|---|
| Session 层 | QueryEngine(类) |
跨多次用户输入,持有 messages / 工具列表 / 权限 |
| Loop 层 | query() → queryLoop()(函数) |
单次用户输入,多轮 LLM 调用直到无 tool_use |
| 迭代层 | State(数据包) |
单轮 LLM 调用结束到下一轮开始之间 |
State(query.ts:204)是 ReAct 每轮迭代间传递的数据包,不是枚举状态机——字段可同时激活:
1 | 工作记忆(每轮必传) |
异常书签触发场景:
- maxOutputTokensOverride:stop_reason = max_tokens(LLM 输出被截断)→ 临时升至 64k 重试当前轮,下轮重置。
- maxOutputTokensRecoveryCount:升 64k 仍不够 → 注入隐藏消息
"Output token limit hit. Resume directly..."让 LLM 续写,最多 3 次,超过放弃。 - hasAttemptedReactiveCompact:API 返 prompt_too_long → 压缩历史重试一次,置 true 后再超限直接以
{ reason: 'prompt_too_long' }退出 loop(用户看到提示,程序不卡死)。退出时不跑 Stop Hook,防死循环。 - stopHookActive:Stop Hook 返回 blocking error(如检测到危险命令)→ 把错误注入 messages 让 LLM 重新回复;标志在重试期间保留,防 compact 清掉。
- autoCompactTracking:主动压缩连续失败 3 次触发熔断——曾有 session 失败 3272 次、每天浪费 25 万次 API 调用。
Agent Loop
1 | Agent Loop (query.ts) |
Agent Message:AssistantMessage
AssistantMessage 是 LLM => Agent 这一流向的信息层最终包装(当然还有其他流向比如tool和user都是属于UserMessage)。
1 | claude.ts 拼好的 block,最终大概是这样: |
所以这里的 AssistantMessage 不是”整轮回复最终版”,而是”某一个 block 收齐后的出队结果“。
如果一轮里依次生成了 thinking → text → tool_use,外层就会依次收到 3 条 AssistantMessage,每条里的 message.content 都只放当前这个 block。
外层字段 type / uuid / timestamp / requestId 是 Claude Code 自己补的运行时信息;
里面的 message 基本还是 Anthropic 原生 message 结构,只是 content 此时只装当前这个刚收齐的 block。
AssistantMessage 样例:
1 | { |
这里的 AssistantMessage 还是”模型产出的消息块”;到了 Ch 06,它里面的 tool_use block 才会被当成真正的”待执行工具调用”继续往下处理。
和 nanobot 对比:nanobot 用适配器把 Anthropic 原生格式转成统一 LLMResponse 抹平结构;Claude Code 基本直接沿用原生 message/block 结构,只在外层补本地元数据,并按 block 粒度逐条 yield。
AssistantMessage 组装流程
Block 是什么:Anthropic API 一次回复的 content 是数组,每个元素是一个 block(thinking / text / tool_use),一次回复可同时包含多个 block。SSE 流式时每个 block 分批到达,claude.ts 用 contentBlocks[] 数组逐步拼装,收齐才封包。
1 | 经 SDK 3 层处理后到达本处,见「SSE 分层传输机制」 |
SSE 分层传输机制
A社的 Anthropic API 服务端返回每条消息长这样(event+data):
1 | event: content_block_start |
SDK 在 claude.ts 和 TCP 之间做了 3 层处理,每层自己做缓冲,不依赖下层,最终确保每一个 block 都是完整的:
1 | TCP 字节流(可在任意字节截断) |
每层自己做缓冲保证完整性,不依赖下层。claude.ts 拿到的是事件流,不是字节流,也不是组装好的 block。
与 nanobot 对比:
| 维度 | nanobot | Claude Code | 优劣 |
|---|---|---|---|
| 文字输出 | 等整个响应结束才返回 | 每个 token 立即 yield | CC ✅ 体验更好 |
| 工具参数传输 | 响应结束后整体解析 | input_json_delta 边收边拼 | CC ✅ 更早可用 |
| 工具结果超限 | 直接截断,内容丢失 | 持久化磁盘,模型可完整读取 | CC ✅ 信息不丢失 |
| Thinking 支持 | 作为字段返回,无流式 | thinking block 独立流式累加 | CC ✅ |
| 复杂度 | 简单直接 | SSE 状态机,多 block 并行 | nanobot ✅ 更易理解 |
| 多 block 回复 | 单次响应只有一段内容 | 一次回复可含 thinking+text+多工具 | CC ✅ 表达能力强 |
Ch 06 - 工具调用系统
Tool 接口设计
定义在 src/Tool.ts:
| 字段 | 用途 | nanobot 有? |
|---|---|---|
name |
工具名,LLM 按名字调用 | ✅ |
prompt() |
生成给 LLM 的工具说明,注入 API 的 tools 字段 | ✅ 但静态字符串 |
description() |
生成给用户看的操作描述,用于权限弹窗展示 | ❌ |
inputSchema (Zod) |
参数结构校验 | ✅ JSON Schema |
call() |
实际执行函数 | ✅ |
isConcurrencySafe() |
调度层用,决定能否并发执行 | ❌ |
isReadOnly() |
权限判断,只读命令不需用户确认 | ❌ |
isDestructive() |
UI 警示用,MCP 列表标红,不影响执行 | ❌ |
interruptBehavior() |
Ctrl+C 时 cancel 立刻停 / block 等完成 | ❌ |
maxResultSizeChars |
结果大小上限,超限持久化磁盘 | ❌ |
nanobot 工具接口扁平,调度层不知道工具”性格”,只能串行。Claude Code 工具自己声明行为,调度层据此决定并发还是串行。
完整工具执行路径(LLM 返回 → 结果追回)
[1] SSE 流识别 tool_use(claude.ts)
1 | 前面已经讲过 |
[2] query.ts 收集(query.ts)
1 | 前面已经讲过,存在AssistantMessage 的 block.type === 'tool_use' → 推入 toolUseBlocks[] |
[3] 分批调度(toolOrchestration.ts)
1 | runTools(toolUseBlocks) |
isConcurrencySafe 由工具自己实现,三种模式:
- 纯读工具 → 硬编码
true;状态变更工具 → 硬编码false;未实现 → 默认false - BashTool 特殊:解析实际命令动态判断,
git log为 true,rm为 false - 调度层只调用,不主观判断
[4] 单工具执行(toolExecution.ts)
1 | runToolUse(单个 block) |
[5] 结果追回(query.ts)
1 | toolResults[] 追加进 messages → continue → 下一轮调 LLM |
nanobot 对比:无权限系统、无 hooks、无并发调度,工具直接串行 call()。
Ch 07 - 流式执行引擎(默认关闭)
实验性引擎(config.gates.streamingToolExecution,默认关闭),LLM 流式返回期间 tool_use block 一到手就立即入队执行,不等流结束。
StreamingToolExecutor 完整流程
1 | 创建:queryLoop 开始时 new StreamingToolExecutor() |
单个 Tool 状态机
ToolStatus:
| 状态 | 含义 | 进入条件 | 流转到 |
|---|---|---|---|
queued |
已入队,等待调度 | addTool() 调用 |
executing |
executing |
执行中 | canExecuteTool() 为 true |
completed |
completed |
执行结束,结果就绪 | collectResults() 完成(含合成错误) |
yielded |
yielded |
结果已消费,终态 | getCompletedResults() 输出后 |
无 |
无失败状态——失败工具收到合成 error tool_result 后直接进入 completed。
并发规则(canExecuteTool):无 executing 工具时任何工具可执行;有 executing 时,新工具和所有 executing 都是 safe 才可并发,否则等待。unsafe 工具独占执行。
工具执行与输出流程
确保流式执行的主要的逻辑在 getCompletedResults 这个方法里。
1 | 核心总结: |
三层 AbortController 架构
重要:StreamingToolExecutor 由 config.gates.streamingToolExecution 控制,默认关闭。关闭时走 runTools() 路径,只有第 1 层 toolUseContext.abortController,无三层结构。
概念层级与 AbortController 对应(父子单向传播:父 abort → 子自动 abort;子 abort → 父不受影响):
1 | Agent 层(整个对话轮次) |
为什么分三层:
1 | 用户 Ctrl+C → 停所有(第 1 层 abort,全链自动 abort) |
Ch 08 - Thinking 推理模式
Thinking Config 与模型支持
定义在 src/utils/thinking.ts,三种类型:
| 类型 | 含义 | 适用 |
|---|---|---|
adaptive |
模型自决推理量 | 新模型默认 |
enabled + budgetTokens |
固定 token 预算 | 旧模型 |
disabled |
关闭 thinking | - |
模型支持范围:
- thinking:1P/Foundry 全部 Claude 4+;3P(Bedrock/Vertex)仅 Sonnet 4+ / Opus 4+
- adaptive 模式:仅 opus-4-6 / sonnet-4-6(未知新 1P 模型默认 true,向前兼容)
API 构造:支持 adaptive 的模型发 { type: 'adaptive' };否则发 { budget_tokens: min(maxOutput-1, budgetTokens) }。
发送给 LLM 的 Thinking Block 约束(3条)
给LLM发的 API 硬约束(违反返回 400):
- 若上下文含 thinking block 的历史,那么本次 budget 必须 > 0
- thinking block 不能是 AssistantMessage 的最后一个 block
- 有工具调用时,
thinking → tool_use → tool_result → 下轮 assistant整链不能断
签名机制:thinking block 内嵌加密签名,绑定模型 + API Key,不可跨模型/跨账号复用。
清除时机:切换 fallback 模型、/login 换账号 → stripSignatureBlocks() 清除历史 thinking block;streaming 中途重试 → yield tombstone 从 UI 和历史中删除。
服务端保留:context-management-2025-06-27 beta,Claude 4+ 且 provider 为 firstParty/Foundry 时启用;Bedrock/Vertex 不触发;compaction 时由服务端保留 thinking block。
Ch 09 - Task 系统
持久化任务看板
不同于 toolUseBlocks[](当轮执行即丢弃),Task 是跨轮次、跨进程的持久化协调机制。核心用途:LLM 将复杂请求拆解为多条 Task,由多个 subAgent 自主认领并行执行。
1 | Task 数据结构(src/utils/tasks.ts) |
High Water Mark:max(文件最大ID, 历史记录ID),防止 reset 后 ID 复用。
taskListId 优先级
存储:~/.claude/tasks/<taskListId>/,每条 Task 一个 JSON 文件,session 结束不清理。taskListId 对 LLM 透明,框架在每次工具调用时内部静默解析。
1 | 优先级(高→低) |
两级锁保证任务原子性
锁完全靠磁盘竞争实现,内存中无锁变量。proper-lockfile 在目标文件旁创建 .lock 文件,OS 保证创建原子性,进程崩溃后有 stale lock 检测(检查 mtime)。
| 锁粒度 | 锁的目标文件 | 用于 |
|---|---|---|
| 任务级锁 | <id>.json(旁边创建 <id>.json.lock) |
updateTask / 普通认领 |
| 列表级锁 | .lock(tasks 目录下预建空文件) |
createTask / resetTask / 严格认领 |
失败时指数退避重试,最多 30 次:
1 | 等待时间 = min(5 × 2^n, 100) ms |
Agent 领取任务执行流程
涉及方法:
| 方法 | 工具 | 作用 |
|---|---|---|
createTask() |
TaskCreateTool | 创建,初始 pending,blocks/blockedBy 为空 |
listTasks() |
TaskListTool | 列出所有 Task(无锁读) |
getTask() |
TaskGetTool | 读取单条完整 Task |
updateTask() |
TaskUpdateTool | 更新字段(status/owner/description 等)| 内部可调 deleteTask 删除 |
blockTask() |
TaskUpdateTool(addBlocks) | 双向写 blocks/blockedBy 建立依赖 |
claimTask() |
- | tasks.ts 内部函数,swarm 严格认领用,含原子 checkAgentBusy;TaskUpdateTool 日常直接用 updateTask 不走此路径 |
以 Task #2 依赖 Task #1 为例:
1 | LLM 拆解请求 → 决定创建两个串行 Task |
框架只查验 blockedBy,不传递结果:跨 Agent 输出靠 description 里约定路径 + 主动读文件,语义层全托管给 LLM。
Ch 10 - Skill 系统
Skill 是 Markdown 模板 + 执行器。用户输入 /simplify,框架把模板展开成 ContentBlockParam[],以 role: user 注入对话——等价于用户手动粘贴一段长 prompt。代码类型是 Command.type === 'prompt'(PromptCommand),关键字段:
1 | PromptCommand 关键字段 |
/simplify 触发后往 messages 插 3 条:标题行(用户可见)、Skill 正文(isMeta: true,仅 LLM 可见)、权限包(allowedTools / model)。
三种来源(loadedFrom):bundled(编译进二进制,启动注册)、disk(.claude/skills/ Markdown 文件)、mcp(stub,跳过)。disk skill 三层优先级:policy > user(~/.claude/skills/)> project(.claude/skills/)。
SkillTool 是模型主动触发的路径:LLM 像调工具一样 { name: 'Skill', input: { name: 'simplify' } },走 fork subAgent,结果以 tool_result 返回。user-invocable: false 的 Skill 只走此路径,用户直接 /skill 报错。
| nanobot | Claude Code | |
|---|---|---|
| 可复用工作流 | 无 | Skill 系统 |
| subAgent 触发 | 手动构造 | fork 自动 / SkillTool |
| 模型主动触发 | 不支持 | SkillTool |
Skill 自定义(disk skill)
Markdown 文件放入 .claude/skills/ 即可,文件名即命令名:
1 | --- |
变量替换:$ARGS / $1 $2 …(用户参数)、${CLAUDE_SKILL_DIR}(skill 所在目录)、${CLAUDE_SESSION_ID}(当前 session)。内联 shell MCP skill 禁用。
执行模式
1 | 用户输入 /simplify |
Ch 11 - Compact:对话压缩
state.messages 随对话积累,发给 API 前经 getMessagesAfterCompactBoundary() 从最后一个 compact boundary 切片,控制实际 token 量。超限时触发压缩,把旧历史替换为摘要 + boundary marker,之后 LLM 只看到摘要和近期对话。
四层压缩结构
第1层 Microcompaction - 每轮 API 调用前静默运行,不生成摘要
公开版:时间触发路径仍运行;ant-only 路径和 legacy 路径已移除。
1 | 时间触发(距上次 assistant 消息超阈值,cache 已冷) |
第2-3层 Autocompact - token 超阈值时触发,生成摘要
触发阈值(以 200k 模型为例):
水位线:
1 | 147k warningThreshold ← 显示用量警告 |
推导来源(均基于 effectiveContextWindow = 200k 总值 - 20k 回复预留 = 180k):
1 | 180k - 20k(WARNING_THRESHOLD_BUFFER_TOKENS) = 160k(Autocompact 关时) |
为什么减 20k:200k context_window 是读写共用总额度(
input_tokens + max_tokens ≤ 200k),摘要调用需预留 20k 给输出,故 input 上限收缩至 180k。
正常流程:147k warningThreshold 触发警告 → 167k Autocompact 介入 → 连续失败 3 次熔断(MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES)→ 之后跳过压缩直到撞 177k blockingLimit → 退出 loop,须手动 /compact。
触发方式:自动(token 用量超阈值)或手动 /compact(可附自定义摘要指令)。
压缩路径:
1 | ├─ 先试 Session Memory Compaction |
第4层 Reactive Compaction - API 真正返回 413 后触发
1 | LLM 返回 prompt_too_long(HTTP 413) |
压缩后结构
buildPostCompactMessages() 把 CompactionResult 组装成新的 state.messages:
1 | [boundaryMarker] type='system',getMessagesAfterCompactBoundary 的切片锚点 |
下一轮 normalizeMessagesForAPI 过滤后,LLM 实际收到:
1 | { role: 'user', content: 'This session is being continued...\nSummary:\n[摘要]' } |
boundary marker(type=’system’)被过滤掉,LLM 完全看不到,只感知到对话从一条摘要 user 消息开始。
完整对话历史同时实时 append 到磁盘 transcript 文件(~/.claude/projects/<项目>/<sessionId>.jsonl),compact 不删除它。summaryMessage 里注明了路径,LLM 需要时可主动读取找回压缩前细节。
1 | state.messages 内存,compact 后替换为摘要 |
Ch 12 - 记忆系统
不管怎么样,记忆系统想做到的永远是跨Session的上下文保存。
三种记忆共同点:用文件存储,靠 prompt 注入读取。写的触发方式各不同——Auto Memory 和 Agent Memory 靠 LLM 自主决定,Session Memory 靠框架 PostSampling hook 自动提取;读的筛选由 Sonnet 做语义匹配,框架负责注入。
三种记忆对比
| Auto Memory | Session Memory | Agent Memory | |
|---|---|---|---|
| 作用域 | 一个 git repo | 一次会话 | user / project / local |
| 存储 | ~/.claude/projects/<repo>/memory/ |
~/.claude/session-memory/<sessionId>.md |
user: ~/.claude/agent-memory/<type>/ project: .claude/agent-memory/<type>/ local: .claude/agent-memory-local/<type>/ |
| 记什么 | 关于用户和项目的知识(偏好、约定、外部系统) | 当前会话关键进展(纯笔记,无 frontmatter) | 该类型 Agent 执行任务中积累的专属经验 |
| 写 | LLM 自主用 Write 工具 | PostSampling hook 自动 fork subAgent 提取 | subAgent 自主用 Write 工具 |
| 读 | 每轮 Sonnet 筛选注入(≤5 个) | Compact 时 Session Memory Compaction 路径读取 | Agent spawn 时一次性注入系统 prompt |
| 公开版 | ✅ | ❌ | ✅ |
Auto Memory 和 Agent Memory 用同一套文件格式(frontmatter + 正文),共用 buildMemoryLines prompt 模板;内容视角不同——前者记”关于用户/项目的知识”,后者记”这类 Agent 执行任务时学到的东西”。
触发流程
① 会话启动(一次性)
1 | CLAUDE.md 文件加载 → getUserContext() |
② 每次用户输入(每轮开始)
1 | startRelevantMemoryPrefetch() 异步发起(不阻塞) |
③ 工具执行循环中(每轮内部)
1 | pendingMemoryPrefetch settled? |
④ 每次 LLM 响应后(PostSampling hook)
1 | extractSessionMemory()(❌ 公开版 feature flag 关) |
⑤ subAgent spawn 时
1 | loadAgentMemoryPrompt(agentType, scope)(同步,在 getSystemPrompt 回调里) |
四类文件格式
Auto Memory 和 Agent Memory 均使用相同格式。两步保存:① 写 .md 文件 ② 在 MEMORY.md 加索引;MEMORY.md 超 200 行截断,每条约 150 字符。
1 | # MEMORY.md(索引,始终加载) |
使用前验证:memory 里提到的文件/函数可能已不存在,先 grep/check 再引用。
Snapshot 机制
团队协作用途:将预置的 Agent 记忆提交到 git,新成员 clone 后 spawn 该类 Agent 时自动初始化,无需手动配置。
1 | # 团队在 git 里提交预置记忆 |
replaceFromSnapshot 是覆盖不是合并——先删光本地所有 .md 再复制,所以 prompt-update 不自动执行,避免覆盖成员自己积累的记忆。
Ch 13 - 上下文管理
每次调用 LLM,Claude Code 把上下文分成三个区域组装进请求——system 字段、首条特殊消息、对话历史——不同类型的上下文注入位置不同。
完整请求结构
1 | ── API Request ────────────────────────────────── |
CLAUDE.md 加载机制
getMemoryFiles() 按优先级从低到高依次加载,越靠后加载的文件 LLM 越关注:
1 | ① Managed /etc/claude-code/CLAUDE.md + .claude/rules/*.md 企业统一策略 |
这里的 User / Project / Local 和 Ch 12 Agent Memory 的 scope 是同一套作用域模型:User 全局、Project 团队共享、Local 个人私有。
区别是 CLAUDE.md 是开发者预先写好的静态指令,框架没有任何机制引导 LLM 自动写入(对比 Auto Memory 有完整的自动写入路径)。
@include 指令:文件内用 @path 引用其他文件,被引用文件插入到引用文件之前。代码块内无效,循环引用有保护,支持相对/绝对/~/ 路径。
getClaudeMds() 把所有文件拼合,格式为 Contents of <path> (<类型描述>):\n\n<内容>,整体注入 userContext.claudeMd。
工具权限上下文
ToolPermissionContext 挂在 AppState 上,把所有来源的权限配置汇总成一个快照,工具调用前统一查这里。
1 | ToolPermissionContext |
对话中点「永远允许」→ 往 alwaysAllowRules.session 追加;/plan 进入只读模式,原 mode 存入 prePlanMode 退出时恢复。
subAgent 的上下文裁剪与隔离
AgentTool spawn subAgent 时,对上下文做一系列裁剪和隔离:
1 | 父 Agent 上下文 |
Ch 14 - Hooks 扩展
Hook 是什么:在约定时机启动子进程,主流程把上下文序列化成 JSON 传入,读取子进程输出决定是否继续——本质是”可执行文件 + stdin/stdout 协议”的旁路回调机制。CC 扩展了一种 prompt hook:触发时机相同,但执行体换成自然语言条件,$ARGUMENTS 替换为工具调用 JSON 后整体作为 UserMessage 发给 Haiku,返回 {ok: true/false} 决定放行或阻断。
来源:settings.json(用户配置)/ Agent·Skill frontmatter(subAgent 启动时注册)/ FunctionHook(CC 内部 TS 回调,不走 shell,session 级临时)
执行器:command(子进程)/ prompt(Haiku)/ agent(subAgent)/ http(外部服务)
事件 × 返回值:
事件挂在工具调用前后(PreToolUse / PostToolUse / PostToolUseFailure)、会话生命周期(SessionStart / Stop)、subAgent 生命周期(SubagentStart / SubagentStop)、权限(PermissionRequest / PermissionDenied)、文件变更、MCP 交互等节点。
返回 JSON 控制主流程:{decision:"block"} 阻断工具 / {continue:false} 终止 Agent / {async:true} 不阻塞后台跑 / hookSpecificOutput 可修改入参或 MCP 输出。**approve 不能绕过 settings.json deny 规则。**
与MAS 交叉部分:subAgent 的 Stop frontmatter hook 自动转为 SubagentStop,可在subAgent 结束后做验证。
Ch 15 - 沙盒与权限系统
Agent 为什么比普通程序更需要沙盒
普通程序攻击面固定可静态分析;Agent 多了一条链路:提示注入 → LLM 行为改变 → 工具产生真实副作用。读一个恶意文件就能触发 rm -rf,这在普通程序里不存在。
三个根源:LLM 输出不可预测 / 工具有真实副作用 / 任意外部内容都能变成”指令”。
权限系统可被说服绕过,沙盒是不可绕过的 OS 级硬边界。
沙盒的两个核心维度与安全兜底
Agent 能造成破坏的只有两条路:写坏文件 或 发出网络请求。
文件系统:进程启动时重新挂载,allowWrite / denyWrite / denyRead / allowRead 四向控制,deny 永远优先。
网络:默认拒绝所有出站,allowedDomains + WebFetch(domain:x) 权限规则合并成白名单。
CC 只做规则收集,实际执行由 @anthropic-ai/sandbox-runtime 负责。
安全兜底:
- 硬编码 denyWrite:settings.json /
.claude/skills// bare git repo 特征文件写死只读,防止 Agent 改权限配置或植入恶意 skill - 命令后清理:
core.fsmonitor是 git 的合法 hook,git 运行时自动执行并继承 CC 权限,可被提示注入利用。对策:启动时记录 cwd 中不存在的特征文件,沙盒命令结束后立刻删除新增的,在 unsandboxed git 运行前关掉时间窗口 - failIfUnavailable:沙盒启不来默认静默降级——安全系统失败要吵闹不能静默
- autoAllowBashIfSandboxed:沙盒启用时跳过 Bash 权限弹窗,OS 已在执行,再弹窗是多余摩擦
权限系统 vs 沙盒:两层防御
沙盒的 OS 实现:限制进程系统调用的内核机制。macOS 用 sandbox-exec,Linux 用 bwrap + socat,Windows 不支持。CC 底层交给闭源的 @anthropic-ai/sandbox-runtime,adapter 只做配置转换。
两层的本质区别:权限系统是 CC 应用层软拦截(可被社会工程学绕过——LLM 说服用户在弹窗点允许);沙盒是 OS 层硬拦截,启动后固化在内核,运行时无法修改。
权限系统的执行形态:工具调用触发时 CC 弹出 3 选 1 交互:Yes(放行一次)/ Yes, during this session(本 session 内全部放行)/ No(拒绝)。用户确认后产生一条 allow 规则,存入五个 destination 之一:
1 | ├─ session → 纯内存,session 结束消失 |
两层联动:权限规则自动同步进沙盒——Edit(/src/**) → allowWrite,WebFetch(domain:x) → 网络白名单。一套规则,两层执行。
默认关闭(sandbox.enabled ?? false):开启需在 settings.json 加 "sandbox": {"enabled": true},仅支持 macOS / Linux / WSL2。
自建 MAS 沙盒的实现路径
CC 用的方案:Linux/WSL2 用 bubblewrap(bwrap)+ socat,macOS 用 sandbox-exec,Windows 不支持。
MAS 特有的问题:沙盒粒度
单 Agent 沙盒粒度清晰(一个进程一个沙盒),MAS 里需要额外决定:
1 | ├─ 每个subAgent 独立沙盒 |
CC 的选择:共享沙盒。subAgent 运行在父 Agent 的沙盒内部(enableWeakerNestedSandbox 配置项印证嵌套沙盒存在),AgentTool 启动subAgent 是同进程内调用,不创建新沙盒实例。subAgent 的隔离靠应用层权限上下文,不是 OS 层。
Ch 16 - MAS 转向 Harness Engineering
CC 用 bun:bundle 的 feature() 在构建时判断 flag,为 false 的代码直接从 bundle 里删掉。对外发布版根本不含这些代码,不是运行时 if/else。以下特性均默认关闭,仅内部版本可用。
Agent 分工的三个维度
协调模式(多 Agent 如何分工)
1 | ├─ 单 Agent 顺序执行 → 默认 REPL |
执行机制(Agent 怎么跑)
1 | ├─ 同进程同步 → AgentTool 默认 |
Agent 特化(角色是什么)
1 | ├─ 通用 → general-purpose |
Coordinator / Worker 详解
是什么:把”思考调度”和”动手执行”拆给两个角色,防止同一 Agent 上下文被执行细节淹没。
能力边界是物理强制的,不靠 prompt 约定:
- Coordinator:无执行工具(无 Bash/Edit/Read),只有 AgentTool / SendMessageTool / TaskStopTool,物理上无法直接操作
- Worker:无全局对话上下文,只收到一个自包含任务,无法影响整体方向
实现方式:同一二进制,CLAUDE_CODE_COORDINATOR_MODE=1 切换角色,注入不同 system prompt + 工具集。
执行流程:
1 | 用户 → Coordinator 拆解任务,并行派 Worker |
Continue vs Spawn:本质是”Worker 的上下文值不值得复用”
1 | 探索文件 = 要修改文件 → Continue |
并发规则:读操作随意并行 / 写操作同组文件同时只跑一个 Worker / 验证可与不同文件区域的实现并行。
MAS 启示:调度者不执行是约束不是建议;Worker 结果异步事件驱动不是同步调用;综合是 Coordinator 唯一不可外包的职责;上下文是资源,Continue/Spawn 是复用决策。
Swarm:tmux 多进程并行
我认为这是Harness Engineering的方向。
创建专属 tmux session(claude-swarm),每个 Agent 对应一个 tmux pane,tmux split-window 切分,每个 pane 是独立 Claude 进程。pane 颜色不同便于区分,创建时有互斥锁防并发竞争,shell 初始化后等 200ms 再启动 Agent。Agent 间通过 SendMessageTool 通信。
KAIROS:事件驱动唤醒
Agent 不被用户主动触发,而是订阅事件后自动唤醒。CC 里两种实现:
- PR 事件:Coordinator 调用
subscribe_pr_activity,GitHub PR 评论 / CI 结果以”用户消息”形式注入对话 - 时间门控(autoDream):满足”距上次执行超过 N 小时 + 积累 N 个 session”自动触发后台记忆整合
Ch 17 - MCP 集成
MCP(Model Context Protocol) 是 Anthropic 主导的开放协议,规范 LLM 与外部工具/数据源之间的通信格式。CC 是纯 MCP Client,不托管 Server。
传输层
6 种传输类型,按使用场景分:
1 | ├─ stdio 本地进程,CC spawn 常驻子进程,管道通信,90% 场景 |
stdio 本质:CC 用 child_process spawn 子进程(如 npx some-mcp-server),往其 stdin 写 JSON 请求,从 stdout 读响应,会话结束才关闭——常驻进程,不是跑完退出的命令。
通信格式
底层协议 JSON-RPC 2.0(所有传输层通用)。工具命名规则 mcp__{serverName}__{toolName}(双下划线),server name 经 normalizeNameForMCP() 净化(非 [a-zA-Z0-9_-] 字符转 _)。工具描述上限 2048 字符,超出截断(OpenAPI 生成的 server 常有 60KB 描述)。
调用示例(CC → github server 的 list_repos):
1 | 请求 CC → stdin: { "jsonrpc":"2.0", "id":1, "method":"tools/call", |
CC 内部用 mcp__github__list_repos 路由,但下发给 server 时用原始名 list_repos,LLM 对来源无感知。
工具注册与合并
启动时 CC 并发连接所有配置 server,各 server 执行 ListTools 后封装为 MCPTool 注入工具池,与内置 Bash/Write 同级,LLM 感知不到来源差异。
配置来源(ConfigScope 6 个):
1 | ├─ local 项目本地(不提交 git) |
连接状态:pending → connected / failed / needs-auth / disabled,各 server 独立,互不影响。
权限与认证
Channel 权限(Telegram/iMessage/Discord):CC 触发权限对话框时同步向 channel server 推送审批请求,人类在手机上回复 yes xxxxx / no xxxxx,server 解析后发结构化事件 notifications/claude/channel/permission,CC 收到后完成审批。批准方是人类,CC 无法自批准。若 channel server 被攻破,攻击者可伪造审批——已知可接受风险(被攻破的 channel 本就能无限注入对话,自批准只是更快,能力上限不变)。
OAuth / XAA:OAuth 用于远程 server 认证,CC 触发浏览器授权流,token 存 keychain,过期自动刷新。XAA(Cross-App Access)用于 claude.ai 托管 server,基于 Claude 账号做跨应用授权,不走标准 OAuth 流。
Ch 18 - 总结:Claude Code 架构全景
1. MAS 调度
CC 支持四种协调模式:单 Agent / 主从委托 / Coordinator-Worker / Swarm 对等并行。Coordinator 模式通过物理约束分离调度与执行——Coordinator 无执行工具,Worker 无全局上下文,分工靠工具集而非 prompt 约定。Swarm 用 tmux 实现真正的独立进程并行,是目前最重的多 Agent 方案。
2. Agent Loop
核心是 query.ts 的 while(true) ReAct 循环,每轮调一次 LLM,有工具调用就执行再循环,无工具则结束。所有扩展能力(Hooks、Thinking、Compact、工具调度)都叠加在这个循环上,不替代它。退出条件有 10 种,覆盖正常结束、用户中断、token 超限等全部场景。
3. 上下文与 Compact
CC 把上下文视为有限资源,设计了四层压缩策略:Microcompaction(每轮必跑,移除图片缩小结果)/ Session Memory(直接存记忆,不调 LLM)/ Traditional(fork subAgent 做全量摘要)/ Reactive(LLM 报错时实时触发)。四种策略按成本从低到高依次尝试,压缩失败才升级。
4. 持久化 Memory
记忆系统分两层:CLAUDE.md 是项目级静态知识,每次请求注入 system prompt;auto memory 是跨会话动态积累的用户/项目偏好,存在 ~/.claude/projects/ 目录下,按 MEMORY.md 索引。两者都是纯文件,LLM 通过读写文件工具维护,没有向量数据库。
6. Task 系统
Task 系统是多 Agent 协作的状态共享基础设施,用 JSON 文件 + 文件锁实现跨进程任务状态同步。父 Agent 通过 TaskCreate 创建任务,subAgent 通过 TaskUpdate 更新进度,父 Agent 轮询感知完成情况。文件锁保证并发写入安全,是轻量级的进程间通信方案。
7. Tool 系统
工具是 CC 的能力单元,每个工具自包含(定义、schema、权限检查、执行、UI 渲染)。调度层支持两种模式:默认 toolOrchestration(safe 工具并发批处理,unsafe 串行,上限 10)和实验性 StreamingToolExecutor(流式中即启动,无并发上限,支持级联取消)。MCP 工具通过标准协议动态接入,与内置工具统一调度。
8. Skill 系统
Skill 是可复用的提示词工作流,以 Markdown 文件形式存储,frontmatter 定义元数据。用户通过斜杠命令触发,CC 读取文件内容展开为完整 prompt 注入对话。Skill 可以定义自己的 Hooks,在subAgent 启动时自动注册,实现工作流级别的生命周期控制。
9. MCP 系统
MCP(Model Context Protocol)是 CC 的工具扩展协议,允许外部服务以标准化方式向 CC 注册工具。CC 作为 MCP 客户端,在启动时连接配置的 MCP 服务器,动态获取工具列表并合并进工具池。MCP 工具与内置工具的权限系统、Hooks、调度机制完全统一。
10. 权限系统与沙盒
权限系统是应用层软拦截,工具调用前检查 settings.json 规则,不符合则弹窗询问用户,决策结果存入五个 destination(session / localSettings / projectSettings / userSettings / cliArg)。沙盒是 OS 层硬拦截,macOS 用 sandbox-exec,Linux 用 bwrap,两者通过规则自动同步联动——权限规则驱动沙盒配置,一套规则两层执行。

