Skip to content

Claude Code Memory 记忆系统深度分析 ​

基于 Claude Code 源码(2026-03-31 快照,512K 行 TypeScript)逆向分析 核心文件:src/memdir/(8个文件)、src/services/extractMemories/(2个)、src/services/SessionMemory/(3个)、src/services/teamMemorySync/(5个)


1. 三层记忆架构总览 ​

Claude Code 的记忆系统不是一个简单的文件存储,而是一个三层递进的知识管理体系:

┌─────────────────────────────────────────────────────────────┐
│  第3层:团队记忆(Team Memory)                               │
│  跨用户、按仓库   │ 服务端同步 │ REST API + 乐观锁           │
│  秘密扫描保护     │ 文件监听器 │ 30种凭证检测规则             │
├─────────────────────────────────────────────────────────────┤
│  第2层:持久记忆(Persistent Memory / extractMemories)       │
│  跨会话、按项目   │ 本地文件   │ 后台 Fork Agent 自动提取     │
│  4种类型分类      │ MEMORY.md 索引 │ AI 相关性召回            │
├─────────────────────────────────────────────────────────────┤
│  第1层:会话记忆(Session Memory)                             │
│  仅当前会话      │ 单个文件   │ 10个固定章节 │ 服务于压缩     │
└─────────────────────────────────────────────────────────────┘
层级范围持久性触发方式存储位置
会话记忆当前会话临时(会话结束即停用)后采样钩子(token+工具调用阈值)会话目录下单文件
持久记忆跨会话、按项目永久(直到手动删除)每轮查询结束时(Stop Hook)~/.claude/projects/<项目>/memory/
团队记忆跨用户、按仓库远程服务器 + 本地镜像文件监听器(2秒去抖)memory/team/ + Anthropic API

2. 持久记忆系统(核心) ​

2.1 记忆文件格式 ​

每条记忆是一个独立的 Markdown 文件,带 YAML frontmatter:

markdown
---
name: 用户偏好-简洁回复
description: 用户不希望在每次回复末尾加总结
type: feedback
---

不要在回复末尾总结刚做了什么,用户能看到 diff。

**Why:** 用户明确要求过"stop summarizing what you just did"
**How to apply:** 所有回复结束时,直接结束,不加回顾性总结。

2.2 四种记忆类型 ​

typescript
const MEMORY_TYPES = ['user', 'feedback', 'project', 'reference'] as const
类型用途保存时机作用域(团队模式)
user用户角色、目标、知识背景了解到用户的身份信息时始终私有
feedback用户对工作方式的指导用户纠正或确认做法时默认私有
project项目进展、目标、事件了解到工作计划、截止日期时偏向团队
reference外部系统指针了解到 Linear/Grafana/Slack 等资源时通常团队

每种类型要求的结构:

  • feedback 和 project 类型必须包含 **Why:** 和 **How to apply:** 行
  • 相对日期必须转换为绝对日期(如"周四"→"2026-03-05")
  • user 类型的示例:"深度 Go 经验,React 新手——用后端类比解释前端概念"

2.3 明确不保存的内容 ​

源码中硬编码了 6 类排除项(即使用户明确要求也不保存):

  1. 代码模式、架构、文件路径、项目结构——可从代码推导
  2. Git 历史、最近变更——git log/git blame 是权威来源
  3. 调试方案或修复步骤——修复在代码中,commit message 有上下文
  4. CLAUDE.md 中已有的内容——不重复
  5. 临时任务细节:进行中的工作、当前对话上下文
  6. 如果用户要求保存 PR 列表或活动摘要,要追问"什么是令人意外的或不明显的?"——只保存那部分

2.4 MEMORY.md 索引机制 ​

MEMORY.md 是索引,不是记忆本身

格式: 纯 Markdown,无 frontmatter。每条一行,约 150 字符以内:

markdown
- [用户偏好-简洁回复](feedback_concise.md) — 不在回复末尾加总结
- [项目-合并冻结](project_freeze.md) — 3月5日起移动端发布冻结

限制:

  • 最大 200 行(MAX_ENTRYPOINT_LINES)
  • 最大 25,000 字节(MAX_ENTRYPOINT_BYTES)
  • 超出时在末尾追加截断警告
  • 截断按行边界执行(不会切断一行的中间)

两步保存流程:

  1. 写入记忆文件(如 feedback_concise.md)
  2. 在 MEMORY.md 中添加索引行

可选的 skipIndex 模式(通过 tengu_moth_copse 特性开关)可跳过第2步。

2.5 存储目录结构 ​

~/.claude/
  projects/
    -Users-charlesqin-Desktop-myproject/   ← sanitizePath(项目根目录)
      memory/                               ← AUTO_MEM_DIRNAME
        MEMORY.md                           ← 索引文件
        user_role.md                        ← 私有记忆文件
        feedback_testing.md
        project_deadline.md
        reference_linear.md
        team/                               ← 团队记忆子目录
          MEMORY.md                         ← 团队索引
          project_api_migration.md
          reference_oncall_board.md
        logs/                               ← KAIROS 每日日志
          2026/
            03/
              2026-03-31.md

2.6 路径安全机制 ​

paths.ts 包含多层安全验证:

安全层检查内容防御目标
绝对路径检查拒绝相对路径路径遍历
根路径检查拒绝长度 < 3 的路径写入系统根目录
UNC 路径检查拒绝 \\server\share 和 //server/shareNTLM 凭证泄露
Null 字节检查拒绝包含 \0 的路径路径截断攻击
Tilde 展开限制拒绝 ~、~/、~/.、~/..匹配整个 HOME
项目设置排除.claude/settings.json 不能设 autoMemoryDirectory恶意仓库重定向写入到 ~/.ssh
NFC 规范化Unicode NFC 标准化macOS 路径不一致
Git 根规范化worktree 共享主仓库的记忆避免重复记忆目录

关键安全设计: projectSettings(提交到仓库的 .claude/settings.json)被有意排除在 autoMemoryDirectory 的信任来源之外。源码注释明确说明原因:

typescript
// SECURITY: projectSettings is intentionally excluded —
// a malicious repo could set autoMemoryDirectory: "~/.ssh"
// and gain write access to sensitive directories

3. 自动记忆提取(extractMemories) ​

3.1 触发时机 ​

用户输入 → 模型响应 → 工具执行 → 循环... → 模型最终响应(无工具调用)
                                                    ↓
                                              Stop Hooks 执行
                                                    ↓
                                        executeExtractMemories()  ← 即发即忘

前置条件(全部满足才执行):

  1. EXTRACT_MEMORIES 编译时特性开关开启
  2. isExtractModeActive() = true(tengu_passport_quail GrowthBook 开关)
  3. isAutoMemoryEnabled() = true
  4. 非子代理(!toolUseContext.agentId)
  5. 非 bare 模式
  6. 自上次提取以来有新的模型可见消息

3.2 互斥机制 ​

typescript
// 如果主代理在对话中已经直接写了记忆文件,跳过自动提取
if (hasMemoryWritesSince(lastMemoryMessageUuid)) {
  // 推进游标但不执行提取——避免覆盖用户显式保存的记忆
  lastMemoryMessageUuid = messages[messages.length - 1].uuid
  return
}

这防止了自动提取和用户手动保存之间的冲突。

3.3 Fork Agent 执行 ​

typescript
const result = await runForkedAgent({
  messages: conversationMessages,
  system: extractionPrompt,
  maxTurns: 5,           // 硬上限:5轮(读1轮 + 写1-4轮)
  canUseTool: createAutoMemCanUseTool(autoMemPath),
  // ... 共享主会话的 prompt cache
})

工具权限白名单:

工具权限
FileRead允许(无限制)
Grep允许(无限制)
Glob允许(无限制)
Bash仅只读命令(ls, find, grep, cat, stat, wc, head, tail)
FileEdit仅限 autoMemPath 内
FileWrite仅限 autoMemPath 内
MCP / Agent / 写入型 Bash全部禁止

3.4 提取节流 ​

typescript
let turnsSinceLastExtraction = 0
const turnsBeforeExtraction = getFeatureValue('tengu_bramble_lintel', 1)

// 每 N 轮查询才执行一次提取(默认每轮都执行)
if (turnsSinceLastExtraction < turnsBeforeExtraction) {
  turnsSinceLastExtraction++
  return
}

3.5 提取后通知 ​

成功提取后,将写入的文件路径列表作为 SystemMemorySavedMessage 追加到主对话中,这样主代理知道记忆已更新。


4. AI 驱动的记忆召回(findRelevantMemories) ​

4.1 召回流程 ​

用户发送消息
  ↓
scanMemoryFiles(memoryDir)
  ↓ 递归读取所有 .md 文件(排除 MEMORY.md)
  ↓ 解析前 30 行 frontmatter(description + type)
  ↓ 按 mtime 降序排序,上限 200 个文件
  ↓
过滤掉 alreadySurfaced(之前轮次已展示的路径)
  ↓
selectRelevantMemories(query, memories, signal, recentTools)
  ↓ 构建文本清单:每行 "- [type] filename (timestamp): description"
  ↓ 附加 "Recently used tools: ..." 部分
  ↓
sideQuery → Sonnet 模型
  ↓ 系统提示:SELECT_MEMORIES_SYSTEM_PROMPT
  ↓ 用户消息:Query + Available memories
  ↓ 结构化输出:{ selected_memories: string[] }
  ↓ max_tokens: 256
  ↓
返回最多 5 条 RelevantMemory(路径 + mtime)

4.2 选择提示词的关键规则 ​

- 返回最多 5 个文件名
- 要有选择性和辨别力;如果不确定,不要包含
- 如果提供了最近使用的工具列表:
  ✗ 不要选择这些工具的使用参考/API 文档(已经在用了)
  ✓ 仍然选择这些工具的警告/陷阱/已知问题

4.3 记忆新鲜度标注 ​

typescript
function memoryAge(mtimeMs: number): string {
  const days = memoryAgeDays(mtimeMs)
  if (days === 0) return 'today'
  if (days === 1) return 'yesterday'
  return `${days} days ago`
}

function memoryFreshnessText(mtimeMs: number): string {
  if (memoryAgeDays(mtimeMs) <= 1) return ''  // 新鲜记忆不加警告
  return `This memory is ${days} days old.
Memories are point-in-time observations, not live state —
claims about code behavior or file:line citations may be outdated.
Verify against current code before asserting as fact.`
}

设计动机(源码注释): 用户报告过过期的记忆被模型当作事实断言。"47 days ago" 比 ISO 时间戳更能触发模型的过期推理。

4.4 召回前的验证要求 ​

源码中注入的系统提示明确要求模型在使用记忆前验证:

- 如果记忆提到一个文件路径 → 检查文件是否存在
- 如果记忆提到一个函数或标志 → grep 搜索它
- 如果用户即将根据你的建议行动 → 先验证
- "记忆说 X 存在"不等于"X 现在存在"
- 活动日志、架构快照是冻结在某个时间点的,优先使用 git log

5. 会话记忆(Session Memory) ​

5.1 与持久记忆的关键区别 ​

维度会话记忆持久记忆
生命周期当前会话跨会话永久
文件数量1 个多个
触发频率每次后采样(需满足阈值)每轮查询结束
主要用途服务于上下文压缩跨会话知识保留
记忆提取者Fork Agent(仅 FileEdit)Fork Agent(Read/Write/Edit/Grep)
格式10个固定章节自由格式 + frontmatter

5.2 触发阈值 ​

typescript
// 默认配置(可通过 GrowthBook tengu_sm_config 远程调整)
const defaults = {
  minimumMessageTokensToInit: 10_000,   // 上下文达到 10K tokens 才激活
  minimumTokensBetweenUpdate: 5_000,    // 每增长 5K tokens 更新一次
  toolCallsBetweenUpdates: 3,           // 且至少 3 次工具调用
}

// 触发条件:
// (token 阈值满足 AND 工具调用阈值满足)
// OR (token 阈值满足 AND 最后一轮助手消息无工具调用)

5.3 会话记忆模板(10 个固定章节) ​

markdown
# Session Memory

## Session Title
_Brief description of what this session is about_

## Current State
_What is the current status of the work_

## Task specification
_Detailed description of the current task requirements_

## Files and Functions
_Key files and functions being worked on_

## Workflow
_Steps being followed or processes in use_

## Errors & Corrections
_Errors encountered and how they were resolved_

## Codebase and System Documentation
_Important codebase patterns, conventions, and system behavior_

## Learnings
_Insights gained during this session_

## Key results
_Important outputs, measurements, or achievements_

## Worklog
_Chronological log of actions taken_

5.4 大小限制 ​

typescript
const MAX_SECTION_LENGTH = 2_000      // 每个章节最大 2K tokens
const MAX_TOTAL_SESSION_MEMORY_TOKENS = 12_000  // 整个文件最大 12K tokens

5.5 与压缩的集成 ​

Session Memory 是上下文压缩的优先路径(参见 sessionMemoryCompact.ts):

  • 自动压缩触发时,优先尝试用 Session Memory 作为摘要
  • 保留最近 10K-40K tokens 的消息
  • 比传统压缩快得多(不调用 LLM),且摘要质量更可预测

5.6 自定义 ​

用户可以自定义模板和提示词:

  • 模板:~/.claude/session-memory/config/template.md
  • 提示词:~/.claude/session-memory/config/prompt.md(支持 替换)

6. 团队记忆同步 ​

6.1 前置条件 ​

同时满足才启用:

  1. TEAMMEM 编译时特性开关开启
  2. isTeamMemoryEnabled() = true(tengu_herring_clock GrowthBook 开关)
  3. 第一方 OAuth + 同时拥有 INFERENCE_SCOPE 和 PROFILE_SCOPE
  4. Git remote 是 github.com(非 GitHub 仓库不同步)

6.2 API 端点 ​

基础 URL: {baseUrl}/api/claude_code/team_memory?repo={owner/repo}
方法参数用途关键状态码
GETrepo={slug}拉取全部团队记忆200 / 304 / 404
GETrepo={slug}&view=hashes仅拉取哈希(轻量探测)200 / 404
PUTrepo={slug}上传记忆条目(upsert)200 / 412 / 413

认证头: Authorization: Bearer {oauthToken}

6.3 同步协议 ​

拉取(Pull) ​

1. GET 请求,携带 ETag(条件请求)
2. 304 = 未变化,跳过
3. 200 = 有更新:
   a. 对每个条目验证路径(防遍历攻击)
   b. 跳过 >250KB 的文件
   c. 跳过本地内容已匹配的文件
   d. 并行写入本地文件系统
   e. 刷新 serverChecksums

推送(Push — Delta 上传 + 乐观锁) ​

1. readLocalTeamMemory(): 遍历 team/ 目录
   → 每个文件扫描秘密(30种规则)
   → 跳过 >250KB 的文件
   → 计算 SHA-256 哈希

2. 计算 delta:仅本地哈希与 serverChecksums 不同的 key

3. 分批上传:贪心装箱,每批 ≤ 200KB (MAX_PUT_BODY_BYTES)

4. 每批携带 If-Match 头(乐观锁)

5. 冲突处理:
   412 Conflict → 探测 ?view=hashes → 刷新 serverChecksums → 重算 delta → 重试(最多 2 次)
   413 Too Many → 学习 serverMaxEntries 上限,截断后重试

6. 冲突策略:本地优先(local-wins)
   → 当前用户正在编辑的内容覆盖服务端

6.4 文件监听器 ​

typescript
// watcher.ts
fs.watch(teamMemDir, { recursive: true })
// macOS: FSEvents(O(1) 文件描述符)
// Linux: inotify(O(子目录数))

// 2 秒去抖:最后一次变更后 2 秒才触发推送

推送抑制: 永久失败(no_oauth、no_repo、4xx 非 409/429)后,抑制后续推送,直到发生文件删除(恢复动作)或会话重启。

6.5 团队 vs 私有的作用域划分 ​

源码注入的系统提示中,每种记忆类型都带有 XML 作用域标签:

xml
<type>
  <name>user</name>
  <scope>always private</scope>
  ...
</type>

<type>
  <name>project</name>
  <scope>strongly bias toward team</scope>
  ...
</type>

额外安全要求:

MUST avoid saving sensitive data within shared team memories

7. 秘密扫描保护 ​

7.1 双层防护 ​

层时机文件行为
写入时拦截FileWrite/FileEdit 调用teamMemSecretGuard.ts阻止写入,返回错误
上传前扫描pushTeamMemory 读取本地文件secretScanner.ts跳过该文件,不上传

7.2 检测的 30 种秘密模式 ​

云服务商:

  • AWS Access Token(A3T/AKIA/ASIA/ABIA/ACCA 前缀)
  • GCP API Key(AIza 前缀)
  • Azure AD Client Secret
  • DigitalOcean PAT / Access Token

AI API:

  • Anthropic API Key(sk-ant-api03- 前缀,运行时拼接避免自身匹配)
  • Anthropic Admin API Key(sk-ant-admin01-)
  • OpenAI API Key(sk-proj/svcacct/admin + T3BlbkFJ 标记)
  • HuggingFace Access Token(hf_)

版本控制:

  • GitHub PAT / Fine-grained PAT / App Token / OAuth / Refresh Token
  • GitLab PAT / Deploy Token

通信平台:

  • Slack Bot/User/App Token(xoxb-/xoxp-/xapp-)
  • Twilio API Key
  • SendGrid API Token

开发工具:

  • npm Access Token
  • PyPI Upload Token
  • Databricks API Token
  • HashiCorp Terraform API Token
  • Pulumi API Token
  • Postman API Token

可观测性:

  • Grafana API Key / Cloud API Token / Service Account Token
  • Sentry User/Org Token

支付:

  • Stripe Access Token(sk_test/live/prod 或 rk_)
  • Shopify Access Token / Shared Secret

密码学:

  • PEM 格式私钥(BEGIN/END PRIVATE KEY 块)

7.3 设计原则 ​

typescript
// 匹配到的秘密值永远不被记录或返回——只返回规则 ID 和人类可读标签
// Anthropic API key 前缀在运行时拼接,避免匹配自身的 excluded-strings 检查:
const prefix = ['sk', 'ant', 'api'].join('-')

8. KAIROS 模式的记忆(每日日志) ​

当 KAIROS(AI 助手模式)激活时,记忆范式完全改变:

普通模式:写独立文件 + 更新 MEMORY.md 索引
KAIROS 模式:追加到 logs/YYYY/MM/YYYY-MM-DD.md 日志文件
  • 只追加,不编辑已有内容
  • 每日一个文件,按日期路径组织
  • 由单独的 /dream 技能在夜间蒸馏日志为主题文件和 MEMORY.md
  • 提示词中使用 YYYY-MM-DD 占位符而非实际日期(因为提示词被缓存,日期变化不会触发失效)

9. 记忆系统注入的完整系统提示词 ​

9.1 个人模式(auto memory) ​

# auto memory

You have a persistent, file-based memory system at `<memoryDir>`.
This directory already exists — write to it directly with the Write tool.

You should build up this memory system over time so that future conversations
can have a complete picture of who the user is, how they'd like to collaborate
with you, what behaviors to avoid or repeat, and the context behind the work.

[如果用户要求记住 → 立即保存]
[如果用户要求忘记 → 找到并删除]

## Types of memory
[4种类型,每种含 description/when_to_save/how_to_use/examples]

## What NOT to save in memory
[6类排除项]

## How to save memories
Step 1: 写入文件(带 frontmatter)
Step 2: 在 MEMORY.md 中添加索引行

## When to access memories
- 看起来相关时
- 用户明确要求时(必须访问)
- 用户说"忽略记忆"时,假装 MEMORY.md 为空

## Before recommending from memory
- 提到文件路径 → 检查文件是否存在
- 提到函数/标志 → grep 搜索
- 用户要行动 → 先验证

## Memory and other forms of persistence
- 实施方案 → 用 Plan,不用记忆
- 当前对话步骤 → 用 Tasks,不用记忆

## Searching past context
[grep 记忆文件,或搜索会话 transcript]

## MEMORY.md
[实际的 MEMORY.md 内容,或"当前为空"]

9.2 团队模式额外内容 ​

在个人模式基础上增加:

## Memory scope
- 私有记忆:<privateDir> — 你的个人偏好、反馈
- 团队记忆:<teamDir> — 项目上下文、外部引用

[类型定义中增加 <scope> XML 标签]

额外安全规则:不得在团队记忆中保存敏感数据

10. 团队记忆的路径安全(深度防御) ​

teamMemPaths.ts 是整个记忆系统中安全措施最密集的文件:

10.1 自定义错误类 ​

typescript
class PathTraversalError extends Error {
  name = 'PathTraversalError'
}

10.2 sanitizePathKey — 服务端提供的相对路径清理 ​

typescript
function sanitizePathKey(key: string): string {
  // 1. 拒绝 null 字节
  // 2. URL 编码遍历检测:解码 %2e%2e%2f 等
  // 3. Unicode 规范化攻击:全角字符 \uFF0E\uFF0E\uFF0F → ../
  // 4. 拒绝反斜杠(Windows 路径遍历)
  // 5. 拒绝绝对路径
}

10.3 realpathDeepestExisting — 符号链接解析 ​

typescript
async function realpathDeepestExisting(path: string): Promise<string> {
  // 逐级向上 realpath() 直到成功
  // 检测悬挂符号链接(link 存在但 target 不存在)
  //   → 安全威胁:writeFile 会跟随链接在 team 目录外创建文件
  // 检测符号链接循环(ELOOP)→ PathTraversalError
  // 不可恢复的错误(EACCES, EIO)→ fail-closed
}

10.4 validateTeamMemWritePath — 双重验证 ​

写入团队记忆文件前的完整验证链:

第1遍:字符串级
  1. Null 字节检查
  2. path.resolve() 解析
  3. startsWith(teamDir) 验证

第2遍:符号链接级
  4. realpathDeepestExisting() 解析实际路径
  5. isRealPathWithinTeamDir() 验证实际路径在团队目录内

通过 → 返回解析后的路径
失败 → 抛出 PathTraversalError

10.5 前缀攻击防护 ​

typescript
// 团队目录路径以分隔符结尾:/foo/team/
// 这样 /foo/team-evil/ 不会通过 startsWith 检查

11. 记忆的生命周期全景 ​

会话开始
  │
  ├─ loadMemoryPrompt()
  │   → 读取 MEMORY.md → 注入系统提示
  │
  ├─ initSessionMemory()
  │   → 注册后采样钩子
  │
  ├─ startTeamMemoryWatcher()
  │   → 拉取服务端 → 启动文件监听
  │
  ▼
每轮对话
  │
  ├─ findRelevantMemories(query)
  │   → 扫描文件 → Sonnet 选择 → 返回最多 5 条
  │   → 新鲜度标注(>1天 → 加过期警告)
  │
  ├─ [模型可能直接读/写记忆文件]
  │   → 写入团队记忆 → checkTeamMemSecrets() 拦截秘密
  │   → 文件监听触发 → 2秒后推送到服务端
  │
  ├─ Session Memory 后采样钩子
  │   → 检查阈值 → Fork Agent 更新单文件
  │
  ├─ 模型最终响应(无工具调用)
  │   → Stop Hooks → executeExtractMemories()
  │   → 互斥检查 → Fork Agent 提取 → 更新持久记忆文件
  │
  ▼
自动压缩触发时
  │
  ├─ trySessionMemoryCompaction()(优先路径)
  │   → 用 Session Memory 内容作为摘要
  │   → 保留最近 10K-40K tokens 的消息
  │
  ├─ compactConversation()(回退路径)
  │   → Fork Agent 生成 9 章节结构化摘要
  │
  ▼
会话结束
  │
  ├─ drainPendingExtraction()
  │   → 等待进行中的记忆提取完成
  │
  └─ 团队记忆推送最后一批变更

12. 关键设计决策总结 ​

决策原因
记忆文件是独立的 .md 文件,不是数据库用户可以直接编辑、Git 追踪、跨工具使用
MEMORY.md 是索引不是记忆防止单文件膨胀,支持截断不丢核心内容
自动提取在 Stop Hook 中即发即忘不阻塞主对话循环
互斥检查(主代理 vs 自动提取)防止覆盖用户显式保存的内容
召回用 Sonnet 不用 Haiku需要理解语义相关性,Haiku 不够准确
新鲜度用"X days ago"不用 ISO 时间模型对相对时间的推理比绝对时间好
项目设置排除 autoMemoryDirectory防止恶意仓库劫持写入路径
团队记忆双重路径验证符号链接攻击需要在文件系统层面检查
秘密模式在运行时拼接避免自身的 excluded-strings 检查
本地优先的冲突策略用户正在编辑 = 最新意图
断路器(MAX_CONFLICT_RETRIES = 2)防止推送冲突的无限重试

来自:AI超元域 | B站频道:https://space.bilibili.com/3493277319825652

基于 Claude Code 源码逆向分析,2026-03-31