基于 claude-code 2.1.70 源码分析
一、文件加载
1.1 加载顺序(loadMemoryFiles / VJ)
所有文件拼接到 prompt 中(不是覆盖),后加载的优先级更高:
| 阶段 | 类型 | 路径 | 备注 |
|---|---|---|---|
| 1 | Managed | 组织管理路径 + managed .claude/rules/ |
组织统一下发 |
| 2 | User | ~/.claude/CLAUDE.md + ~/.claude/rules/ |
用户全局 |
| 3 | Project | 目录树遍历:{dir}/CLAUDE.md、{dir}/.claude/CLAUDE.md、{dir}/.claude/rules/ |
从外→内 |
| 4 | Local | 目录树遍历:{dir}/CLAUDE.local.md |
不提交 git |
| 5 | Additional | 环境变量 CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD 指定的目录 |
额外目录 |
| 6 | AutoMem | session 级自动记忆文件 | 如 MEMORY.md |
1.2 projectRoot
就是 realpathSync(process.cwd()),与 git 无关。
目录遍历从 projectRoot 向上走到文件系统根 /,reverse 后从外到内加载。
1.3 去重与过滤
processedPathsSet 防止同一文件加载两次claudeMdExcludes设置:glob 模式排除特定 CLAUDE.md(仅对 User/Project/Local 生效)- git worktree 中跳过原始仓库目录下的文件
.claude/skills/被 gitignore 的会跳过- 外部
@file引用递归深度上限 = 5 层(dm9 = 5)
1.4 外部引用(@-syntax)
@file.md
@./path/to/file
- User / Managed 来源:始终允许
- Project / Local 来源:需要用户审批(
hasClaudeMdExternalIncludesApproved) - 支持大量文件扩展名(.md, .txt, .json, .yaml, .ts, .py, .go 等)
二、注入策略
2.1 注入方式
CLAUDE.md 的内容不在 system prompt 中,而是作为第一条 user 消息注入:
Fy1(messages, userContext)
↓
[
{ ← 插入的第一条消息
role: "user",
isMeta: true, ← 标记为 meta(不显示给用户)
content: "<system-reminder>
As you answer the user's questions, you can use the following context:
# claudeMd
Codebase and user instructions are shown below...
Contents of ~/.claude/CLAUDE.md (user's private global instructions):
...实际内容...
# currentDate
Today's date is 2026-03-07.
IMPORTANT: this context may or may not be relevant to your tasks.
</system-reminder>"
},
...原有消息 ← 用户对话消息跟在后面
]
2.2 每个文件的标注
注入时每个文件会标注来源类型:
| 类型 | 标注文字 |
|---|---|
| Project | (project instructions, checked into the codebase) |
| Local | (user's private project instructions, not checked in) |
| AutoMem | (user's auto-memory, persists across conversations) |
| User / Managed | (user's private global instructions for all projects) |
2.3 头部强调语
Codebase and user instructions are shown below. Be sure to adhere to these instructions.
IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.
2.4 system prompt 中的 auto memory 部分
buildMemorySection()(_X1)放在 system prompt 的 dynamic section 中,只包含 auto memory 的使用说明(如何保存/读取记忆),不包含实际 CLAUDE.md 文件内容。
三、缓存与刷新
3.1 Memoize 机制
V_() (构建 userContext) 和 VJ() (loadMemoryFiles) 都被 V8() memoize 包装。
同一 session 内,如果缓存未清除,每轮对话复用同一份数据。
3.2 缓存清除时机
| 场景 | 位置 | 说明 |
|---|---|---|
| 编辑了 CLAUDE.md | P0.js:256 — F84() |
同时清除 V_ 和 oO 缓存 |
| 日期变更(跨天) | IV.js:276 — f6Y() |
currentDate 需要更新 |
| 对话压缩(compact) | vOq.js:110 |
压缩后需要重新注入 |
| session 重置 | Nd8.js:24 — id8() |
clearSessionCaches |
| conversation fork | vOq.js:175,187 |
分叉时刷新 |
缓存清除后,下一次 V_() 调用会重新读取所有 CLAUDE.md 文件。
四、与压缩(Compact)的关系
4.1 CLAUDE.md 内容不参与压缩摘要
压缩的对象是对话消息数组(messages)。CLAUDE.md 的 <system-reminder> 消息是通过 Fy1() 在发送 API 请求时实时拼接的,不在消息数组中持久存在。
压缩流程中的消息处理:
h = [...cS(X)] ← cS: 从最后一个 compact_boundary 截取消息
(丢弃更早的消息,保留边界之后的)
h = microcompact(h) ← 目前是 noop
autocompact(h, ...) ← 如果 token 超阈值,生成摘要
↓
wn(compactionResult) = [
boundaryMarker, ← 新的压缩边界标记
...summaryMessages, ← AI 生成的摘要
...messagesToKeep, ← 保留的原始消息
...attachments, ← 最近文件、任务状态等
...hookResults, ← hook 输出
]
↓
Fy1(h, Y) ← 压缩后,重新拼上 CLAUDE.md 的 system-reminder
(因为缓存已清除,会重新读取文件)
4.2 关键点
- 压缩时
V_.cache.clear?.()清除缓存 → 下次调用重新读取 CLAUDE.md - CLAUDE.md 内容每次 API 调用都会通过
Fy1()重新注入,不依赖消息历史 - 即使对话被压缩到只剩摘要,CLAUDE.md 的指令仍然完整保留
五、写入/编辑追踪
5.1 Write Tool 检测
write-tool.ts:298:
if (resolvedPath.endsWith(`${PATH_SEPARATOR}CLAUDE.md`)) {
telemetry("tengu_write_claudemd", {});
}
5.2 Edit Tool 检测
edit-tool.ts:423:同样的检测逻辑。
写入/编辑 CLAUDE.md 后会触发 F84() 清除缓存,使下轮对话获取最新内容。
六、大小警告与诊断
6.1 大文件警告
- 阈值:40,000 字符(
Pl = 40000,注意 src 注释中写的 15,000 是旧值) - 超过阈值的文件会在 REPL 启动时显示警告:
⚠ Large CLAUDE.md will impact performance (45,000 chars > 40,000) • /memory to edit
6.2 Ultra CLAUDE.md 警告
- 阈值:3,000 字符(
OZ6 = 3000) - 针对标记为
IMPORTANT的内容,超过阈值时警告:
⚠ CLAUDE.md entries marked as IMPORTANT exceed 3,000 chars
注:F96() 当前返回 null,v6Y() 返回 [],说明 ultra_claude_md 功能尚未启用,是预留的。
6.3 Token 统计
gfz() 函数统计所有 CLAUDE.md 文件的 token 数:
- 对每个文件调用 token 计数
- 汇总为
claudeMdTokens和memoryFileDetails - 用于
tengu_context_size遥测
七、Settings Sync(设置同步)
7.1 同步路径
USER_SETTINGS: "~/.claude/settings.json"
USER_MEMORY: "~/.claude/CLAUDE.md" ← 同步用户级 CLAUDE.md
projectSettings: "projects/{hash}/.claude/settings.local.json"
projectMemory: "projects/{hash}/CLAUDE.local.md" ← 同步项目级 local
7.2 同步流程
- 检查 OAuth token 是否有所需权限
- GET
/api/claude_code/user_settings - 解析响应(Zod schema 校验)
- 写入本地文件
- 最多重试 3 次,指数退避
- 单文件最大 512KB(
ibq = 512000)
八、Onboarding 中的安全检查
8.1 外部引用审批对话框
启动时如果检测到项目 CLAUDE.md 中有 @ 外部引用:
┌─ Allow external CLAUDE.md file imports? ─┐
│ │
│ This project's CLAUDE.md imports files │
│ outside the current working directory. │
│ Never allow this for third-party repos. │
│ │
│ [Yes, allow] [No, disable] │
└───────────────────────────────────────────┘
- 接受 →
hasClaudeMdExternalIncludesApproved: true - 拒绝 → 外部引用被忽略
8.2 Workspace Trust
外部引用审批在 workspace trust 之后才检查。
九、Rules 目录
.claude/rules/ 目录支持:
.md文件作为额外指令- 条件规则:通过 YAML frontmatter 的
paths字段定义 glob 模式,只在匹配文件路径时激活 - 递归加载,支持子目录
- Managed / User / Project 三个层级都有独立的 rules 目录
十、完整数据流图
claude 启动
│
├─ onboarding
│ ├─ workspace trust check
│ └─ external includes 审批对话框
│
├─ REPL mount → V_() 预热缓存
│ └─ x84() → VJ() = loadMemoryFiles()
│ ├─ 1. Managed CLAUDE.md + rules/
│ ├─ 2. User ~/.claude/CLAUDE.md + rules/
│ ├─ 3. Project 目录树遍历 (外→内)
│ ├─ 4. Local CLAUDE.local.md
│ ├─ 5. Additional directories
│ └─ 6. AutoMem
│ → 拼接为 claudeMd 字符串(带路径和类型标注)
│ → 返回 { claudeMd, currentDate }
│
├─ 每轮 onQuery
│ ├─ V_() [memoized,通常命中缓存]
│ ├─ EW() = buildDefaultSystemPrompt
│ │ └─ _X1() = buildMemorySection (auto memory 说明)
│ ├─ VQ() = assembleSystemPrompt
│ └─ MC() = mainConversationLoop
│ ├─ cS(messages) ← 从 compact boundary 截取
│ ├─ autocompact() ← token 超阈值则压缩
│ │ └─ V_.cache.clear() ← 压缩后清缓存
│ └─ callModel({
│ messages: Fy1(h, Y), ← 每次 API 调用都注入 <system-reminder>
│ systemPrompt: ...,
│ })
│
├─ 编辑 CLAUDE.md 时
│ ├─ telemetry("tengu_write_claudemd")
│ └─ F84() → V_.cache.clear() + VJ.cache.clear()
│ → 下轮对话重新读取
│
└─ Settings Sync
└─ 远程下载 → 覆盖本地 ~/.claude/CLAUDE.md
十一、单轮对话完整流程
11.1 总览
用户按下 Enter
│
▼
┌─────────────────────────────────────────────────────┐
│ Phase 1: 输入处理 (onSubmit → onQuery) │
├─────────────────────────────────────────────────────┤
│ Phase 2: 并行上下文加载 │
│ ├── EW() → 系统提示词 (system prompt) │
│ ├── V_() → 用户上下文 (CLAUDE.md + currentDate) │
│ └── oO() → 系统上下文 (gitStatus 等) │
├─────────────────────────────────────────────────────┤
│ Phase 3: MC 主循环 (while true) │
│ ├── cS() → 截取最近消息 │
│ ├── autocompact → 自动压缩检查 │
│ ├── Fy1() → 注入 CLAUDE.md (临时, 不持久化) │
│ ├── callModel → API 调用 + 流式响应 │
│ ├── tool execution → 工具执行 │
│ ├── attachments → 附件计算 │
│ └── merge → 合并消息, 继续下一轮或退出 │
└─────────────────────────────────────────────────────┘
11.2 Phase 1: 输入处理
onSubmit (tj, Z6A.js:1141)
onSubmit(inputText)
│
├── 1. Slash command 检查:如果输入以 / 开头, 走 slash command 分支
├── 2. 输入预处理:解析 @file 引用、图片粘贴等
└── 3. 调用 onQuery(processedInput)
onQuery (iP, Z6A.js:1022)
onQuery(input)
│
├── 1. 并发检查:如果已有进行中的请求, 排队等待
├── 2. 构造 user message:createUserMessage({ content, uuid, ... })
├── 3. 追加到 messages 状态:oq(prev => [...prev, newUserMsg])
│ 同时更新 VO.current (messages ref)
└── 4. 调用 sj(messages) 进入主流程
11.3 Phase 2: 并行上下文加载 (sj, Z6A.js:959)
const [systemPrompt, userContext, systemContext] = await Promise.all([
EW(), // 系统提示词组装
V_(), // CLAUDE.md + currentDate (memoized)
oO(), // gitStatus 等运行时上下文
]);
| 加载项 | 函数 | 内容 |
|---|---|---|
| 系统提示词 | EW() |
角色定义、工具说明、代码风格、MCP 工具、环境信息 |
| 用户上下文 | V_() |
所有 CLAUDE.md 拼接 + currentDate(带 memoize) |
| 系统上下文 | oO() |
gitStatus、环境变量等 |
11.4 Phase 3: MC 主循环 (Hqz, q_q.js)
async function Hqz(messages, systemPrompt, userContext, systemContext) {
let h, p, d;
while (true) {
// ──── Step 1: 截取有效消息 ────
h = [...cS(messages)];
// cS = getMessagesAfterLastCompaction
// 找最后一个 compact_boundary, 只取其后的消息
// ──── Step 2: 自动压缩检查 ────
await autocompact(h, ...);
// token 数 >= (model_max - output - 13000) → 触发压缩
// ──── Step 3: CLAUDE.md 注入 (核心!) ────
const apiMessages = Fy1(h, userContext);
// 纯函数, 返回: [claudeMd_system_reminder_msg, ...h]
// 返回值不写回 messages 状态!
// ──── Step 4: API 调用 ────
const response = await callModel({
system: systemPrompt,
messages: apiMessages, // 包含临时注入的 CLAUDE.md
});
// ──── Step 5: 流式响应处理 ────
p = []; // assistant 回复消息
for await (const event of response.stream) {
processStreamEvent(event);
// 文本块 → 追加到当前 assistant message
// tool_use 块 → 记录待执行工具
// compact_boundary → 重置消息数组
}
// ──── Step 6: 工具执行 ────
if (hasToolUse(p)) {
d = []; // tool results + attachments
for (const toolUse of extractToolUses(p)) {
// 权限检查 → 执行工具 → 收集结果
const result = await executeTool(toolUse);
d.push(createToolResult(result));
}
// ──── Step 7: 附件计算 ────
const attachments = await j6Y(messages, toolResults);
// changed_files, diagnostics, nested_memory 等
// ultra_claude_md 目前返回空数组 (未启用)
d.push(...attachmentMessages);
}
// ──── Step 8: 合并消息 ────
messages = [...h, ...p, ...d];
// h = 历史消息, p = assistant 回复, d = 工具结果 + 附件
// ──── Step 9: 循环判断 ────
if (!hasToolUse(p)) {
break; // 没有工具调用 → 本轮结束
}
// 有工具调用 → 继续下一次迭代 (agent loop)
}
return messages;
}
11.5 CLAUDE.md 注入详解 (Fy1, cli.js:522160)
function Fy1(messages, userContext) {
if (!userContext || !userContext.claudeMd) return messages;
const reminderMsg = createUserMessage({
content: wrapSystemReminder(reminderContent),
// 包裹在 <system-reminder>...</system-reminder> 中
isMeta: true, // 不显示在 UI 中
});
return [reminderMsg, ...messages]; // 纯函数, 不修改原数组
}
关键特性:
- 纯函数:返回新数组,不修改原始 messages
- 临时注入:返回值仅用于当前 API 调用,不持久化到 REPL 状态
- 每次迭代重新注入:while 循环每次迭代都重新执行 Fy1
- 始终最新:V_() 缓存清除后,下次注入就是最新内容
11.6 CLAUDE.md 与压缩的隔离
messages 数组 (REPL 状态)
┌──────────────────────┐
│ user msg 1 │
│ assistant msg 1 │
│ tool_result 1 │ ← 这些消息参与压缩
│ user msg 2 │
│ ... │
└──────────────────────┘
│
┌─────────┴──────────┐
│ │
Fy1() 注入 压缩 (Uf6)
│ │
▼ ▼
┌────────────────┐ ┌────────────────┐
│ CLAUDE.md msg │ │ (无 CLAUDE.md) │
│ user msg 1 │ │ user msg 1 │
│ ... │ │ ... │
└────────────────┘ └────────────────┘
│ │
▼ ▼
发送给模型 API 发送给压缩 API
(模型看到 CLAUDE.md) (压缩不含 CLAUDE.md)
11.7 完整时序图
User REPL (Z6A) sj/MC Hqz (主循环) API
│ │ │ │ │
│── Enter ───────────→ │ │ │ │
│ onSubmit(tj) │ │ │
│ onQuery(iP) │ │ │
│ append user msg │ │ │
│ │── sj() ────────→ │ │ │
│ │ Promise.all([ │ │
│ │ EW(), V_(), oO() │ │
│ │ ]) │ │
│ │ │── MC() ──────→ │ │
│ │ │ while(true) { │
│ │ │ h = cS(messages) │
│ │ │ autocompact(h) │
│ │ │ apiMsgs = Fy1(h, ctx) │
│ │ │ │── callModel ──→ │
│ │ │ │ ←── stream ────│
│ ← stream text ──────│←──────────────────│←─────────────── │ │
│ │ │ if (tool_use) { │
│ │ │ execute tool │
│ │ │ compute attachments │
│ │ │ merge messages │
│ │ │ continue; │
│ │ │ } │
│ │ │ break; // 无工具, 结束 │
│ │ │ } │
│ │ │ ← return ───── │ │
│ │ ← update msgs ───│ │ │
│ ← render final ─────│ │ │ │
源码文件索引
| 概念 | 函数/变量 | 文件 |
|---|---|---|
| REPL 入口 | tj (onSubmit), iP (onQuery) |
Z6A.js |
| 消息状态 | VO.current, oq |
Z6A.js |
| 上下文加载 | sj |
Z6A.js:959 |
| 系统提示词 | EW() |
Z6A.js |
| 用户上下文 | V_() (memoized) |
Te.js:87 |
| CLAUDE.md 加载 | VJ() / loadMemoryFiles |
oQ6.js |
| CLAUDE.md 注入 | Fy1() |
cli.js:522160 |
| 主循环 | Hqz() |
q_q.js:75 |
| 消息截取 | cS() |
base-system-prompt.ts |
| 自动压缩触发 | w04() |
mG1.js:65 |
| 压缩执行 | Uf6(), zP4() |
w16.js |
| 消息标准化 | mD() |
cli.js:531729 |
| 流事件处理 | pu (processStreamEvent) |
Z6A.js:926 |
| 附件计算 | j6Y() |
IV.js:38 |
| 缓存清除 | id8(), F84(), f6Y() |
Nd8.js, P0.js, IV.js |
| projectRoot | $_() = realpathSync(cwd) |
LAA.js:208 |