加载顺序总览
主入口源码文件:
核心函数:
- getMemoryFiles
- getMemoryFilesForNestedDirectory
- processMemoryFile
- processMdRules
- processConditionedMdRules
“会话启动时”的主加载顺序,可以把源码逻辑概括成下面这段伪代码:
// file: src/utils/claudemd.ts
export const getMemoryFiles = memoize(
async (forceIncludeExternal: boolean = false): Promise<MemoryFileInfo[]> => {
result = []
processedPaths = new Set()
// 1. 先加载 Managed
processMemoryFile(getMemoryPath('Managed'), 'Managed', ...)
processMdRules({ rulesDir: getManagedClaudeRulesDir(), type: 'Managed', ... })
// 2. 再加载 User
if (isSettingSourceEnabled('userSettings')) {
processMemoryFile(getMemoryPath('User'), 'User', ...)
processMdRules({ rulesDir: getUserClaudeRulesDir(), type: 'User', ... })
}
// 3. 从 current working directory 一路向上收集目录
dirs = [cwd, parent(cwd), parent(parent(cwd)), ..., root child]
// 4. 按“从高层目录到当前目录”的顺序遍历
for dir in reverse(dirs) {
if (isSettingSourceEnabled('projectSettings')) {
processMemoryFile(join(dir, 'CLAUDE.md'), 'Project', ...)
processMemoryFile(join(dir, '.claude', 'CLAUDE.md'), 'Project', ...)
processMdRules({ rulesDir: join(dir, '.claude', 'rules'), type: 'Project', ... })
}
if (isSettingSourceEnabled('localSettings')) {
processMemoryFile(join(dir, 'CLAUDE.local.md'), 'Local', ...)
}
}
// 5. 如果开启了额外目录记忆加载,再处理 --add-dir
if (CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD enabled) {
for dir in additionalDirs {
processMemoryFile(join(dir, 'CLAUDE.md'), 'Project', ...)
processMemoryFile(join(dir, '.claude', 'CLAUDE.md'), 'Project', ...)
processMdRules({ rulesDir: join(dir, '.claude', 'rules'), type: 'Project', ... })
processMemoryFile(join(dir, 'CLAUDE.local.md'), 'Local', ...)
}
}
return result
},
)
因此可以把 CLAUDE.md 的实际加载顺序概括成一句话:
- 先按
Managed -> User -> Project -> Local做会话级默认加载 - Project / Local 会沿当前工作目录一路向上查找
- 当 Claude 读取目标子目录文件时,再通过 nested memory 补充该目录下的
CLAUDE.md/CLAUDE.local.md/.claude/rules/*.md
“读取某个子目录文件时”的按需补充加载,见 子目录中的 CLAUDE.md 何时加载。
四类指令来源
Managed policy
这是组织级、机器级的统一指令,优先用于公司安全、合规、组织级规范。
常见位置:
- macOS:
/Library/Application Support/ClaudeCode/CLAUDE.md - Linux / WSL:
/etc/claude-code/CLAUDE.md - Windows:
C:\Program Files\ClaudeCode\CLAUDE.md
对应源码里的类型是 Managed,在 getMemoryFiles() 里最先加载:
const managedClaudeMd = getMemoryPath('Managed')
result.push(
...(await processMemoryFile(
managedClaudeMd,
'Managed',
processedPaths,
includeExternal,
)),
)
它的特点是:
- 组织统一管理
- 对当前机器上的所有用户生效
- 不属于项目仓库内容
- 在整体加载顺序里最早进入
User instructions
这是用户级、跨项目的全局个人指令。
典型位置:
~/.claude/CLAUDE.md~/.claude/rules/*.md
用途通常是:
- 你的个人代码风格偏好
- 你习惯的工作流
- 适用于所有项目的长期偏好
在代码里,这一层只有在 userSettings 开启时才加载。
if (isSettingSourceEnabled('userSettings')) {
const userClaudeMd = getMemoryPath('User')
...
}
userSettings 不是通过 CLAUDE.md 文件内部字段开关控制的,而是通过“当前会话允许加载哪些 setting source”来控制。
对应源码文件和位置:
--setting-sources参数解析: main.tsx:484- 参数值到 source 的映射: utils/settings/constants.ts:128
- 默认允许的 source 列表: bootstrap/state.ts:313
简化后的控制逻辑可以写成:
// file: src/main.tsx
const settingSourcesArg = eagerParseCliFlag('--setting-sources')
if (settingSourcesArg !== undefined) {
const sources = parseSettingSourcesFlag(settingSourcesArg)
setAllowedSettingSources(sources)
}
// file: src/utils/settings/constants.ts
isSettingSourceEnabled('userSettings') =
getEnabledSettingSources().includes('userSettings')
其中 parseSettingSourcesFlag() 支持的 CLI 值只有三个:
userprojectlocal
映射关系是:
user->userSettingsproject->projectSettingslocal->localSettings
userSettings 的默认值
在普通 CLI 启动路径里,默认的 allowedSettingSources 是:
allowedSettingSources: [
'userSettings',
'projectSettings',
'localSettings',
'flagSettings',
'policySettings',
]
这意味着如果你什么参数都不传,userSettings 默认就是开启的。
对应效果就是:
- 会加载
~/.claude/CLAUDE.md - 会加载
~/.claude/rules/*.md
如何显式开启 userSettings
虽然默认已经开启,但如果你想显式指定,可以通过命令参数:
claude --setting-sources user
或者:
claude --setting-sources user,project,local
这两种写法都会让 isSettingSourceEnabled('userSettings') 返回 true。
如何显式不设置 user
如果你想明确不加载用户级指令,就不要把 user 写进 --setting-sources 列表。
例如:
claude --setting-sources project,local
此时:
projectSettings开启localSettings开启userSettings关闭
于是 ~/.claude/CLAUDE.md 和 ~/.claude/rules/*.md 都不会进入加载链路。
如果写成:
claude --setting-sources project
那就只保留项目级来源,不加载 user 和 local。
另外,parseSettingSourcesFlag('') 会返回空数组。也就是说,从逻辑上讲“一个都不启用”也是可能的;只是 CLI 通常不会手动这样传。即便如此,flagSettings 和 policySettings 仍会由 getEnabledSettingSources() 强制补回。
这里要注意几个结论:
- CLI 默认是开启
userSettings的 userSettings的开关入口是--setting-sources- 如果显式通过
--setting-sources排除了user,这一层就不会加载 - “不设置 user”最直接的方式就是:
claude --setting-sources project,local
Project instructions
这是团队共享、随仓库版本控制分发的项目级指令。
常见位置有两个:
./CLAUDE.md./.claude/CLAUDE.md
还包括项目规则目录:
./.claude/rules/*.md
这层的用途通常是:
- 项目架构说明
- build / test / lint 命令
- 团队共享规范
- 某些目录或文件类型的规则
实现上,Claude Code 会从当前工作目录开始一路向上遍历到根目录,在每一层都尝试读取:
CLAUDE.md.claude/CLAUDE.md.claude/rules/*.md
对应实现见 utils/claudemd.ts 之后:
const dirs: string[] = []
const originalCwd = getOriginalCwd()
let currentDir = originalCwd
while (currentDir !== parse(currentDir).root) {
dirs.push(currentDir)
currentDir = dirname(currentDir)
}
for (const dir of dirs.reverse()) {
if (isSettingSourceEnabled('projectSettings') && !skipProject) {
const projectPath = join(dir, 'CLAUDE.md')
...
const dotClaudePath = join(dir, '.claude', 'CLAUDE.md')
...
const rulesDir = join(dir, '.claude', 'rules')
...
}
}
这里有两个关键点:
- 不是只加载当前目录一个文件,而是会沿着目录树向上加载祖先目录中的项目级指令
.claude/rules/*.md和CLAUDE.md属于同一套 instruction 体系,不是额外附件
Local instructions
这是当前项目下仅对个人生效的私有指令。
位置通常是:
./CLAUDE.local.md
典型用途:
- 个人 sandbox URL
- 私有测试账号
- 你在当前项目中的个人偏好
官方文档建议把它加入 .gitignore,不要提交到仓库。
代码里它对应 Local 类型,并在每层目录中、项目指令之后加载,见 utils/claudemd.ts:
if (isSettingSourceEnabled('localSettings')) {
const localPath = join(dir, 'CLAUDE.local.md')
result.push(
...(await processMemoryFile(
localPath,
'Local',
processedPaths,
includeExternal,
)),
)
}
这也意味着:
- 它和祖先目录里的
CLAUDE.local.md一样会被一起发现 - 在同一层目录里,它会比
CLAUDE.md更晚进入结果列表 - 如果规则冲突,通常“后加载的更靠后,影响更强”
放置建议
根据官方总结,可以把使用场景理解成下面四类:
| 范围 | 位置 | 适合放什么 |
|---|---|---|
| Managed policy | 系统级固定路径 | 组织安全规范、合规约束、公司通用标准 |
| User instructions | ~/.claude/CLAUDE.md | 个人跨项目偏好 |
| Project instructions | ./CLAUDE.md 或 ./.claude/CLAUDE.md | 团队共享的项目约束 |
| Local instructions | ./CLAUDE.local.md | 当前项目下仅你自己使用的信息 |
简单理解:
- 想让整个组织统一生效,放 Managed
- 想让自己所有项目都生效,放 User
- 想让团队共享,放 Project
- 想只在当前项目里对自己生效,放 Local
实际加载机制
当前实现里的实际加载顺序
源码中的总体顺序是:
ManagedUserProjectLocal
并且注释明确说明:
- later-loaded files take precedence over earlier ones
所以更准确地说,不是“高层覆盖低层”,而是“后加载的优先级更高”。这就形成了:
- 组织级提供底线规则
- 用户级提供全局个人偏好
- 项目级补充团队共享约束
- 本地级最后叠加个人项目特化
不只是根目录:还会沿祖先目录向上查找
这一点很重要。
Claude Code 不是只在当前项目根目录找一次 CLAUDE.md。它的行为是:
- 以
originalCwd为起点 - 一路向上走到文件系统根目录
- 每层都尝试读取 project 和 local 相关文件
所以如果当前工作目录是:
/repo/apps/web
那么它可能会依次发现:
/repo/CLAUDE.md/repo/CLAUDE.local.md/repo/apps/CLAUDE.md/repo/apps/web/CLAUDE.md
这也是为什么 monorepo 里可以用“根规则 + 子目录规则”的方式分层组织。
子目录中的 CLAUDE.md 何时加载
官方文档提到:子目录中的 CLAUDE.md 不是总在会话启动时全部加载,而是会在 Claude 读取该子目录文件时按需进入上下文。
调用链大致如下:
- Claude 准备处理一个具体文件路径
- 附件流程调用
getNestedMemoryAttachmentsForFile(filePath, ...) - 这个函数根据
filePath计算从CWD -> 目标目录的目录链 - 再对这些目录逐个调用
getMemoryFilesForNestedDirectory(dir, filePath, ...) - 最后把结果包装成
nested_memoryattachments 注入上下文
“读取目标文件时触发”的代码片段在 utils/attachments.ts:1792:
async function getNestedMemoryAttachmentsForFile(
filePath: string,
toolUseContext: ToolUseContext,
appState: { toolPermissionContext: ToolPermissionContext },
): Promise<Attachment[]> {
const processedPaths = new Set<string>()
const originalCwd = getOriginalCwd()
const { nestedDirs, cwdLevelDirs } = getDirectoriesToProcess(
filePath,
originalCwd,
)
for (const dir of nestedDirs) {
const memoryFiles = await getMemoryFilesForNestedDirectory(
dir,
filePath,
processedPaths,
)
attachments.push(
...memoryFilesToAttachments(memoryFiles, toolUseContext, filePath),
)
}
}
比如,IDE 打开的文件就会触发这个流程,见 utils/attachments.ts:1877:
const nestedMemoryAttachments = await getNestedMemoryAttachmentsForFile(
ideSelection.filePath,
toolUseContext,
appState,
)
负责找CLAUDE.md的是 utils/claudemd.ts:1249 的 getMemoryFilesForNestedDirectory(...):
export async function getMemoryFilesForNestedDirectory(
dir: string,
targetPath: string,
processedPaths: Set<string>,
): Promise<MemoryFileInfo[]> {
if (isSettingSourceEnabled('projectSettings')) {
processMemoryFile(join(dir, 'CLAUDE.md'), 'Project', ...)
processMemoryFile(join(dir, '.claude', 'CLAUDE.md'), 'Project', ...)
}
if (isSettingSourceEnabled('localSettings')) {
processMemoryFile(join(dir, 'CLAUDE.local.md'), 'Local', ...)
}
processMdRules({ rulesDir: join(dir, '.claude', 'rules'), type: 'Project', ... })
processConditionedMdRules(targetPath, join(dir, '.claude', 'rules'), 'Project', ...)
}
所以这部分更准确的理解应该是:
- 会话启动时:加载当前工作目录向上的祖先链
- 文件读取或处理目标文件路径时:附件流程基于
filePath触发 nested memory,再补充目标子目录对应的CLAUDE.md/CLAUDE.local.md/.claude/rules/*.md
.claude/rules/*.md 也是同一套加载路径的一部分
当前实现里,.claude/rules/*.md 会被一起发现:
- User 级:
~/.claude/rules/*.md - Project 级:
./.claude/rules/*.md
而且它们支持两种模式:
- 无
pathsfrontmatter:无条件规则,启动时加载 - 有
pathsfrontmatter:条件规则,按目标文件路径匹配后加载
这说明“放在哪里”不仅是文件路径问题,也包含“是否拆成规则目录”的结构设计问题。
--add-dir 的特殊情况
默认情况下,--add-dir 只是额外授权目录访问,不会自动加载这些目录下的 CLAUDE.md。
只有开启环境变量后,才会把 additional directories 的 memory 文件也加载进来:
CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1
对应实现见 utils/claudemd.ts:
if (isEnvTruthy(process.env.CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD)) {
const additionalDirs = getAdditionalDirectoriesForClaudeMd()
...
}
所以:
--add-dir不等于自动加载额外目录的指令文件- 还需要一个额外开关控制的环境变量
worktree 场景的特殊处理
这个问题的产生是claude code自身代码行为导致的,所以这个场景就是它自己本身的补丁:
- 使用
claude -w <name>创建 worktree - Claude Code 把 worktree 放到
.claude/worktrees/<name>/ - 这个路径位于主仓库目录内部
这个问题对应的就是源码注释里提到的 issue:
issue 里的核心现象是:
claude -w <name>创建的 worktree 位于.claude/worktrees/<name>/- Claude Code 向上扫描祖先目录时,会同时命中:
- 内层 worktree root
- 外层主仓库 root
- 于是同一套 project-level 文件被重复加载
被重复的通常是这些已检入版本控制的文件:
CLAUDE.md.claude/CLAUDE.md.claude/rules/*.md
所以这段逻辑不是在泛化处理“任意目录下的 git worktree”,而是在专门修复 claude -w 生成的 nested worktree 问题。
一个更直观的例子
假设 claude -w feat 生成的目录关系是:
/repo <- 主仓库 root(canonicalRoot)
/repo/.claude/worktrees/feat <- 当前 worktree root(gitRoot)
如果当前工作目录在:
/repo/.claude/worktrees/feat/apps/web
那么向上遍历时,路径链可能同时经过:
/repo/.claude/worktrees/feat/repo
这两个位置会各自呈现出一套 project-level 文件:
/repo/.claude/worktrees/feat/CLAUDE.md/repo/.claude/worktrees/feat/.claude/rules/*.md/repo/CLAUDE.md/repo/.claude/rules/*.md
从 issue 描述看,这两边通常还是“同一份 git 内容的两份路径表现”。因此如果继续同时加载,实质上就是把同一套规则重复注入两次。
源码是怎么判断“这是嵌套 worktree”的
对应实现见 utils/claudemd.ts:868:
const gitRoot = findGitRoot(originalCwd)
const canonicalRoot = findCanonicalGitRoot(originalCwd)
const isNestedWorktree =
gitRoot !== null &&
canonicalRoot !== null &&
normalizePathForComparison(gitRoot) !==
normalizePathForComparison(canonicalRoot) &&
pathInWorkingPath(gitRoot, canonicalRoot)
这段判断的意思可以概括成:
gitRoot:当前 worktree 认为自己的 git 根目录canonicalRoot:主仓库的规范根目录- 如果两者不相等,并且
gitRoot位于canonicalRoot之内 - 那就说明这是一个“嵌套在主仓库内部的 worktree”
也就是说,这里判断的是:
- 当前会话是否处在一个 nested worktree 里
而不是:
- 当前仓库是否只是普通意义上的任意 git worktree
真正跳过的是哪些文件
接下来源码会为每个祖先目录计算一个 skipProject:
const skipProject =
isNestedWorktree &&
pathInWorkingPath(dir, canonicalRoot) &&
!pathInWorkingPath(dir, gitRoot)
它的含义是:
- 当前确实是 nested worktree
- 当前正在处理的这个祖先目录
dir位于主仓库里面 - 但这个目录并不在当前 worktree 里面
只要满足这三个条件,就认为这个目录属于“主仓库中高于当前 .claude/worktrees/<name> worktree 的那一段路径”,于是跳过它的 project-level memory。
然后真正的加载判断是:
if (isSettingSourceEnabled('projectSettings') && !skipProject) {
processMemoryFile(join(dir, 'CLAUDE.md'), 'Project', ...)
processMemoryFile(join(dir, '.claude', 'CLAUDE.md'), 'Project', ...)
processMdRules({ rulesDir: join(dir, '.claude', 'rules'), type: 'Project', ... })
}
所以被跳过的是:
dir/CLAUDE.mddir/.claude/CLAUDE.mddir/.claude/rules/*.md
也就是所有 Project 类型、且来自主仓库上层路径的已检入规则。
为什么 CLAUDE.local.md 没一起跳过
这是这一节最容易误解的地方。
源码并没有把 Local 和 Project 一起跳过,而是只对 Project 应用 skipProject。
原因在源码注释里写得很清楚:
CLAUDE.local.md通常是 gitignored- 它往往只存在于主仓库工作目录,而不一定存在于每个 worktree checkout 中
因此如果连 Local 一起跳过,反而可能把用户真正想保留的个人本地偏好丢掉。
所以这个策略不是“worktree 上层路径都不读”,而是更精确地说:
- 跳过主仓库上层路径里的
Project级已检入规则,避免重复 - 保留
Local级私有规则,避免把个人本地说明丢掉
这一节可以压缩成一句话
在 claude -w 创建的 nested worktree 场景下,Claude Code 会跳过“主仓库中位于当前 .claude/worktrees/<name> worktree 之外”的 project-level CLAUDE.md 和 .claude/rules/*.md,避免同一套已检入规则被重复加载;但 CLAUDE.local.md 仍然保留,因为它通常是私有的、gitignored 的本地说明。
结论
“CLAUDE.md 放哪里”在 Claude Code 里并不是一个简单的文档组织问题,而是一个直接影响上下文注入范围的问题。
可以记成这套最实用的规则:
- 组织统一规则:放 Managed policy 路径
- 个人跨项目偏好:放
~/.claude/CLAUDE.md - 团队共享项目规则:放
./CLAUDE.md或./.claude/CLAUDE.md - 当前项目下你的私有偏好:放
./CLAUDE.local.md - 更细粒度的模块规则:拆到
.claude/rules/*.md
而在实现层面,Claude Code 会:
- 先按
Managed -> User -> Project -> Local顺序加载 - 对当前工作目录一路向上扫描祖先目录
- 对读取到的子目录文件再补充 nested memory
- 在特定条件下再处理
--add-dir和 worktree 特殊逻辑
所以,选择正确的放置位置,本质上是在决定“CLAUDE.md 应该作用于谁、作用到哪一层、在什么时机进入模型上下文”。