基于 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 去重与过滤

  • processedPaths Set 防止同一文件加载两次
  • 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:256F84() 同时清除 V_ 和 oO 缓存
日期变更(跨天) IV.js:276f6Y() currentDate 需要更新
对话压缩(compact) vOq.js:110 压缩后需要重新注入
session 重置 Nd8.js:24id8() 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() 当前返回 nullv6Y() 返回 [],说明 ultra_claude_md 功能尚未启用,是预留的。

6.3 Token 统计

gfz() 函数统计所有 CLAUDE.md 文件的 token 数:

  • 对每个文件调用 token 计数
  • 汇总为 claudeMdTokensmemoryFileDetails
  • 用于 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 同步流程

  1. 检查 OAuth token 是否有所需权限
  2. GET /api/claude_code/user_settings
  3. 解析响应(Zod schema 校验)
  4. 写入本地文件
  5. 最多重试 3 次,指数退避
  6. 单文件最大 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