深入 Claude Code 源码:CLAUDE.md 的 10 个真相
大多数人写 CLAUDE.md 的方式是:随便写几条规则,感觉效果不明显,然后放弃。
你不知道这些文字在 Claude 眼里长什么样、被放在哪里、和其他指令的优先级关系是什么。
今天我带各位从源码层面,彻底拆解 CLAUDE.md 的底层机制。所有结论都有源码出处,不是猜的。
1. CLAUDE.md 不在 System Prompt 里
这是最大的认知误区。
大多数人以为 CLAUDE.md 的内容被拼接到 system prompt 中。不是的。
源码中,CLAUDE.md 通过 prependUserContext() 函数注入(src/utils/api.ts:449-474):
exportfunctionprependUserContext(
messages: Message[],
context: { [k: string]: string },
): Message[] {
if (Object.entries(context).length === 0) {
return messages
}
return [
createUserMessage({
content: `<system-reminder>
As you answer the user's questions, you can use the following context:
${Object.entries(context)
.map(([key, value]) => `# ${key}\n${value}`)
.join('\n')}
IMPORTANT: this context may or may not be relevant to your tasks.
You should not respond to this context unless it is highly relevant
to your task.
</system-reminder>`,
isMeta: true,
}),
...messages,
]
}
你的 CLAUDE.md 被包装在 <system-reminder> 标签里,作为第一条 user message 插入到对话开头。不是 system prompt,是 user message。
这样会导致几个直接后果:
-
CLAUDE.md 的优先级低于 system prompt。 System prompt 是模型的”宪法”,user message 是”法律”。当两者冲突时,system prompt 通常胜出。 -
模型被告知这些内容”可能不相关” —— 注意那句 IMPORTANT: this context may or may not be relevant to your tasks。这是一个显式的降权信号。
但是 —— CLAUDE.md 的开头有一句强指令(src/utils/claudemd.ts:89-90):
const MEMORY_INSTRUCTION_PROMPT =
'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.'
“OVERRIDE any default behavior” + “MUST follow them exactly” —— 这是在用强语气对抗降权信号。效果取决于指令的具体性:越具体的指令越容易被遵守,越模糊的越容易被忽略。
所以别在 CLAUDE.md 里写”尽量简洁”这种模糊指令。写”回答不超过 3 句话”或”不要添加注释到未修改的代码”这种可验证的具体指令。
2. 从当前目录向上遍历到根目录
CLAUDE.md 不只是项目根目录的一个文件。Claude Code 从当前目录开始,逐级向上遍历到文件系统根目录,在每一级检查以下文件(src/utils/claudemd.ts:790-934):
每一级目录检查:
├── CLAUDE.md → Project 类型(可版本控制)
├── .claude/CLAUDE.md → Project 类型(可版本控制)
├── .claude/rules/*.md → Project 类型(可版本控制,递归扫描子目录)
└── CLAUDE.local.md → Local 类型(不应提交到版本控制)
加上全局位置:
~/.claude/CLAUDE.md → User 类型(个人全局指令)
~/.claude/rules/*.md → User 类型(个人全局规则)
/etc/claude-code/CLAUDE.md → Managed 类型(企业管理员策略)
/etc/claude-code/.claude/rules/*.md → Managed 类型
核心代码逻辑:
exportconst getMemoryFiles = memoize(
async (forceIncludeExternal: boolean = false): Promise<MemoryFileInfo[]> => {
const result: MemoryFileInfo[] = []
const processedPaths = new Set<string>()
// 1. 先加载 Managed(企业策略)
const managedClaudeMd = getMemoryPath('Managed')
result.push(...(await processMemoryFile(managedClaudeMd, 'Managed', ...)))
// 2. 加载 User(个人全局)
if (isSettingSourceEnabled('userSettings')) {
const userClaudeMd = getMemoryPath('User')
result.push(...(await processMemoryFile(userClaudeMd, 'User', ...)))
}
// 3. 从 CWD 向上遍历,每级加载 Project 和 Local
let currentDir = originalCwd
while (currentDir !== parse(currentDir).root) {
dirs.push(currentDir)
currentDir = dirname(currentDir)
}
// 反转后从根目录向下处理
for (const dir of dirs.reverse()) {
// 加载 CLAUDE.md, .claude/CLAUDE.md, .claude/rules/*.md, CLAUDE.local.md
}
},
)
加载顺序决定优先级。源码注释写得很清楚:
Files are loaded in reverse order of priority, i.e. the latest files are highest priority with the model paying more attention to them.
“最后加载的优先级最高” —— 因为模型对靠近输入末尾的内容关注度更高(recency bias)。
完整的加载顺序(从先到后,优先级从低到高):
1. /etc/claude-code/CLAUDE.md (Managed - 企业策略)
2. /etc/claude-code/.claude/rules/*.md (Managed)
3. ~/.claude/CLAUDE.md (User - 个人全局)
4. ~/.claude/rules/*.md (User)
5. /repo-root/CLAUDE.md (Project - 仓库根)
6. /repo-root/.claude/CLAUDE.md (Project)
7. /repo-root/.claude/rules/*.md (Project)
8. /repo-root/src/CLAUDE.md (Project - 更近的目录)
9. /repo-root/src/.claude/rules/*.md (Project)
10. /repo-root/src/feature/CLAUDE.md (Project - 当前目录)
11. /repo-root/src/feature/CLAUDE.local.md (Local - 最高优先级)
实战建议:
-
规则放在仓库根目录的 .claude/rules/下 -
个人偏好(语言、风格)放在 ~/.claude/CLAUDE.md -
子目录特定规则(比如 frontend/CLAUDE.md说”用 React 不用 Vue”)放在对应目录 -
离当前工作目录越近的文件,优先级越高
3. @include 指令引入外部文件
CLAUDE.md 支持 @ 语法引入其他文件(src/utils/claudemd.ts:451-535):
# 我的项目规则
@./coding-standards.md
@~/global-rules.md
@/etc/team-rules/backend.md
解析逻辑的核心正则:
functionextractIncludePathsFromTokens(tokens, basePath): string[] {
const absolutePaths = new Set<string>()
functionextractPathsFromText(textContent: string) {
const includeRegex = /(?:^|\s)@((?:[^\s\\]|\\ )+)/g
let match
while ((match = includeRegex.exec(textContent)) !== null) {
let path = match[1]
// 去掉 fragment 标识符
const hashIndex = path.indexOf('#')
if (hashIndex !== -1) path = path.substring(0, hashIndex)
// 反转义空格
path = path.replace(/\\ /g, ' ')
// 支持 @./path, @~/path, @/path, @path
const isValidPath =
path.startsWith('./') ||
path.startsWith('~/') ||
(path.startsWith('/') && path !== '/') ||
(!path.startsWith('@') &&
!path.match(/^[#%^&*()]+/) &&
path.match(/^[a-zA-Z0-9._-]/))
if (isValidPath) {
const resolvedPath = expandPath(path, dirname(basePath))
absolutePaths.add(resolvedPath)
}
}
}
// ...
}
路径规则:
-
@./path或@path— 相对于当前 CLAUDE.md 文件的路径 -
@~/path— 相对于 home 目录 -
@/path— 绝对路径 -
支持转义空格: @./my\ file.md -
支持 #fragment后缀(会被忽略):@./rules.md#section-1
安全限制:
代码块免疫 —— @ 指令只在 Markdown 的文本节点中生效。代码块和行内代码中的 @ 不会被解析:
if (element.type === 'code' || element.type === 'codespan') {
continue// 跳过代码块
}
最大递归深度 —— MAX_INCLUDE_DEPTH = 5(src/utils/claudemd.ts:537),防止无限嵌套。
扩展名白名单 —— 只能引入文本文件,支持 .md、.txt、.json、.yaml、.ts、.py、.go 等约 80 种。二进制文件被静默忽略。
不存在的文件静默忽略 —— 不会报错,不会中断加载。
可以用 @include 把 CLAUDE.md 拆成模块:
# 项目根目录 CLAUDE.md
@.claude/rules/code-style.md
@.claude/rules/git-conventions.md
@.claude/rules/testing.md
4. Memory 系统的硬编码限制
Memory 系统和 CLAUDE.md 是两套独立的机制,但它们在提示词中紧挨着。
Memory 通过 loadMemoryPrompt() 加载,注入到 system prompt 的动态部分。注意:Memory 在 system prompt 中,CLAUDE.md 在 user message 中,两者的权重天然不同。
MEMORY.md 有硬编码的截断限制(src/memdir/memdir.ts:34-38):
exportconst ENTRYPOINT_NAME = 'MEMORY.md'
exportconst MAX_ENTRYPOINT_LINES = 200
// ~125 chars/line at 200 lines. At p97 today; catches long-line indexes that
// slip past the line cap (p100 observed: 197KB under 200 lines).
exportconst MAX_ENTRYPOINT_BYTES = 25_000
截断逻辑(truncateEntrypointContent):
-
先按行截断 —— 超过 200 行就砍到 200 行 -
再按字节截断 —— 超过 25KB 就在最后一个换行符处切断 -
截断后追加警告:
> WARNING: MEMORY.md is 350 lines (limit: 200). Only part of it was loaded.
> Keep index entries to one line under ~200 chars; move detail into topic files.
所以 MEMORY.md 应该当索引用,不是写内容的地方。每条记忆写一行,控制在 200 字符以内,详细内容放单独的 topic 文件。
错误写法:
## 用户偏好
用户是一名高级 Go 开发者,有 10 年经验,主要在 Linux 环境下工作。
偏好简洁代码,不喜欢过度抽象。使用 Neovim 编辑器...(大量详细描述)
正确写法:
- [User Profile](user_profile.md) — 高级 Go 开发者,10年经验,偏好简洁
5. System Prompt 的分层架构
Claude Code 的 system prompt 由多个模块拼接(src/constants/prompts.ts:444-577),分三层缓存策略:
┌─── 静态层(全局可缓存,跨用户复用)───────────────────────────┐
│ 1. 身份介绍:"You are Claude Code..." │
│ 2. 系统规则(prompt injection 警告等) │
│ 3. 编码任务指南 │
│ 4. 操作谨慎性指南 │
│ 5. 工具使用指南 │
│ 6. 语气风格 │
│ 7. 输出效率 │
├─── SYSTEM_PROMPT_DYNAMIC_BOUNDARY ──────────────────────┤
│ │
│ ── 动态层(会话级缓存,/clear 或 /compact 后重算)── │
│ 8. 会话指导 │
│ 9. ★ Memory 内容(loadMemoryPrompt) │
│ 10. 环境信息(平台、shell、模型名、Git 状态) │
│ 11. 语言偏好 │
│ 12. 输出样式 │
│ │
│ ── 易变层(每轮重算,不缓存)── │
│ 13. MCP 服务器指令 │
│ 14. 暂存板指令 │
│ 15. 函数结果清除提醒 │
│ 16. 工具结果摘要 │
└──────────────────────────────────────────────────────────┘
┌─── User Message 层 ─────────────────────────────────────┐
│ ★ CLAUDE.md 内容(包装在 <system-reminder> 中) │
│ 作为对话的第一条 user message │
├──────────────────────────────────────────────────────────┤
│ 用户的第一条实际消息 │
│ ...后续对话... │
└──────────────────────────────────────────────────────────┘
源码中,静态层和动态层之间有一个明确的边界标记:
return [
// --- Static content (cacheable) ---
getSimpleIntroSection(outputStyleConfig),
getSimpleSystemSection(),
getSimpleDoingTasksSection(),
getActionsSection(),
getUsingYourToolsSection(enabledTools),
getSimpleToneAndStyleSection(),
getOutputEfficiencySection(),
// === BOUNDARY MARKER ===
...(shouldUseGlobalCacheScope() ? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY] : []),
// --- Dynamic content ---
...resolvedDynamicSections,
]
注意两个 ★ 标记的位置:
-
Memory 在 system prompt 的动态层(第 9 位) -
CLAUDE.md 在 user message 层(对话的第一条消息)
Memory 是 system prompt 的一部分,模型视为”系统级指令”。CLAUDE.md 是 user message,模型视为”用户上下文”。但 CLAUDE.md 有那句 “OVERRIDE any default behavior” 来提升权重。
6. 缓存经济学
三层结构直接影响 API 成本。
静态层 使用 cacheScope: 'global' —— 跨所有用户和组织复用:
// src/utils/api.ts:392-394
const staticJoined = staticBlocks.join('\n\n')
if (staticJoined)
result.push({ text: staticJoined, cacheScope: 'global' })
动态层 使用 cacheScope: 'org' —— 在你的组织/账户内复用。
易变层 标记为 DANGEROUS_uncached —— 每轮重新计算,且源码要求必须注明破坏原因:
DANGEROUS_uncachedSystemPromptSection(
'mcp_instructions',
() => getMcpInstructionsSection(mcpClients),
'MCP servers connect/disconnect between turns', // 必须说明原因
)
CLAUDE.md 在 user message 中,不在 system prompt 中,所以它不影响 system prompt 的缓存。 你修改 CLAUDE.md 不会导致 system prompt 缓存失效。但 CLAUDE.md 本身作为 user message 的一部分,会被 prompt caching 机制缓存 —— 只要内容不变,后续轮次不需要重新处理这些 token。
几个注意点:
-
不要频繁修改 CLAUDE.md —— 每次修改都会使 user message 缓存失效 -
MCP 服务器配置变化是”免费的” —— 它在易变层,本来就每轮重算 -
/compact和/clear会清除所有动态层缓存 —— Memory、环境信息等都会重新加载
7. .claude/rules/ 的递归发现
.claude/rules/ 目录支持子目录递归(src/utils/claudemd.ts:697-780):
.claude/
├── CLAUDE.md
└── rules/
├── general.md
├── frontend/
│ ├── react.md
│ └── testing.md
└── backend/
├── api-design.md
└── database.md
所有 .md 文件都会被递归发现和加载。非 .md 文件被忽略。
核心代码:
exportasyncfunctionprocessMdRules({
rulesDir,
type,
processedPaths,
visitedDirs = new Set(),
}: { ... }): Promise<MemoryFileInfo[]> {
if (visitedDirs.has(rulesDir)) {
return [] // 循环检测
}
const { resolvedPath: resolvedRulesDir, isSymlink } =
safeResolvePath(fs, rulesDir)
visitedDirs.add(rulesDir)
if (isSymlink) {
visitedDirs.add(resolvedRulesDir) // 同时记录 symlink 真实路径
}
for (const entry of entries) {
if (isDirectory) {
result.push(...(await processMdRules({ // 递归子目录
rulesDir: resolvedEntryPath,
visitedDirs,
})))
} elseif (isFile && entry.name.endsWith('.md')) {
// 处理 .md 文件
}
}
}
rules 目录有一个特殊的 symlink 安全 处理:代码会解析 symlink 的真实路径,并在 visitedDirs 中同时记录原始路径和解析后的路径,防止 symlink 循环。
8. Git Worktree 去重
如果你在 Git worktree 中工作,CLAUDE.md 的发现有一个巧妙的去重处理(src/utils/claudemd.ts:859-884):
// When running from a git worktree nested inside its main repo,
// the upward walk passes through both the worktree root and the
// main repo root. Both contain checked-in files like CLAUDE.md,
// so the same content gets loaded twice.
const gitRoot = findGitRoot(originalCwd)
const canonicalRoot = findCanonicalGitRoot(originalCwd)
const isNestedWorktree =
gitRoot !== null &&
canonicalRoot !== null &&
normalizePathForComparison(gitRoot) !==
normalizePathForComparison(canonicalRoot) &&
pathInWorkingPath(gitRoot, canonicalRoot)
for (const dir of dirs.reverse()) {
const skipProject =
isNestedWorktree &&
pathInWorkingPath(dir, canonicalRoot) &&
!pathInWorkingPath(dir, gitRoot)
if (isSettingSourceEnabled('projectSettings') && !skipProject) {
// 加载 CLAUDE.md ...
}
}
当你在嵌套 worktree 中时(worktree 目录在主仓库目录内部),向上遍历会经过 worktree 根目录和主仓库根目录。两者都有 CLAUDE.md,会导致同一份内容被加载两次。
Claude Code 会检测这种情况,跳过主仓库中的 Project 类型文件 —— 因为 worktree 已经有自己的 checkout。
但 CLAUDE.local.md 不受影响 —— 它是 gitignore 的,只存在于主仓库,不会被 worktree 复制。
9. 几个隐藏功能
–bare 模式跳过 CLAUDE.md
claude --bare 或设置 CLAUDE_CODE_DISABLE_CLAUDE_MDS=1,所有 CLAUDE.md 都不会加载。--bare 的语义是”跳过我没明确要求的东西”。
settings.json 可以关闭特定来源
if (isSettingSourceEnabled('projectSettings')) {
// 才会加载 Project 类型的 CLAUDE.md
}
if (isSettingSourceEnabled('localSettings')) {
// 才会加载 Local 类型的 CLAUDE.local.md
}
企业管理员可以通过 policy settings 禁用 Project 或 Local 类型的 CLAUDE.md。如果你的指令不生效,检查 settings.json 的 settingSources 配置。
远程 Feature Flag 可以跳过 Project 和 Local
const skipProjectLevel = getFeatureValue_CACHED_MAY_BE_STALE(
'tengu_paper_halyard',
false,
)
// 如果开启,过滤掉 Project 和 Local 类型
Anthropic 内部有一个 feature flag tengu_paper_halyard(src/utils/claudemd.ts:1159,src/utils/attachments.ts:1824),开启后会跳过所有 Project 和 Local 类型的 CLAUDE.md。这个 flag 目前默认 false,但说明 Anthropic 保留了从服务端禁用项目级指令的能力。
10. 我的最佳实践
模块化你的 CLAUDE.md
# 项目根目录 CLAUDE.md
@.claude/rules/code-style.md
@.claude/rules/git-workflow.md
@.claude/rules/testing.md
每个规则文件一个主题,一个文件不超过 50 行。
写具体的、可验证的指令
不要写:
代码要简洁
要写:
- 函数不超过 30 行
- 不要添加注释到你没有修改的代码行
- commit message 用中文,不超过 50 字
利用优先级层级
全局偏好 放 ~/.claude/CLAUDE.md:
- 用中文回答
- 不要在回答末尾总结你做了什么
项目规则 放 项目根目录/.claude/rules/:
# .claude/rules/backend.md
- 使用 Go 1.22+ 的新特性
- 错误处理用 errors.Join 而不是 fmt.Errorf
- 测试用 testify
子目录规则 放对应目录的 CLAUDE.md:
# frontend/CLAUDE.md
- 组件用函数组件,不用 class 组件
- 状态管理用 zustand
MEMORY.md 当索引用
每条记忆一行,不超过 200 字符。详细内容放单独文件。控制总行数不超过 200 行。
总结
CLAUDE.md 远不只是一个配置文件。它有完整的加载优先级体系、模块化 include 机制、缓存策略,以及和 Memory 系统的协作关系。
理解这些底层机制后,你可以:
-
根据优先级层级合理组织指令 —— 近的覆盖远的 -
用 @include 拆分规则模块 —— 保持可维护性 -
写具体可验证的指令 —— 对抗降权信号 -
把 MEMORY.md 当索引 —— 避免截断丢失内容 -
理解缓存影响 —— 避免不必要的成本
掌握了这些,你的 CLAUDE.md 就不再是”感觉有效”,而是确定有效。
夜雨聆风