Skip to content
Go back

CLAUDE.md 加载路径

加载顺序总览

主入口源码文件:

核心函数:

  • 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 的实际加载顺序概括成一句话:

  1. 先按 Managed -> User -> Project -> Local 做会话级默认加载
  2. Project / Local 会沿当前工作目录一路向上查找
  3. 当 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”来控制。

对应源码文件和位置:

简化后的控制逻辑可以写成:

// 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 值只有三个:

  • user
  • project
  • local

映射关系是:

  • user -> userSettings
  • project -> projectSettings
  • local -> 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 通常不会手动这样传。即便如此,flagSettingspolicySettings 仍会由 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/*.mdCLAUDE.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

实际加载机制

当前实现里的实际加载顺序

源码中的总体顺序是:

  1. Managed
  2. User
  3. Project
  4. Local

并且注释明确说明:

  • later-loaded files take precedence over earlier ones

所以更准确地说,不是“高层覆盖低层”,而是“后加载的优先级更高”。这就形成了:

  • 组织级提供底线规则
  • 用户级提供全局个人偏好
  • 项目级补充团队共享约束
  • 本地级最后叠加个人项目特化

不只是根目录:还会沿祖先目录向上查找

这一点很重要。

Claude Code 不是只在当前项目根目录找一次 CLAUDE.md。它的行为是:

  1. originalCwd 为起点
  2. 一路向上走到文件系统根目录
  3. 每层都尝试读取 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 读取该子目录文件时按需进入上下文。

调用链大致如下:

  1. Claude 准备处理一个具体文件路径
  2. 附件流程调用 getNestedMemoryAttachmentsForFile(filePath, ...)
  3. 这个函数根据 filePath 计算从 CWD -> 目标目录 的目录链
  4. 再对这些目录逐个调用 getMemoryFilesForNestedDirectory(dir, filePath, ...)
  5. 最后把结果包装成 nested_memory attachments 注入上下文

“读取目标文件时触发”的代码片段在 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:1249getMemoryFilesForNestedDirectory(...)

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

而且它们支持两种模式:

  • paths frontmatter:无条件规则,启动时加载
  • paths frontmatter:条件规则,按目标文件路径匹配后加载

这说明“放在哪里”不仅是文件路径问题,也包含“是否拆成规则目录”的结构设计问题。

--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.md
  • dir/.claude/CLAUDE.md
  • dir/.claude/rules/*.md

也就是所有 Project 类型、且来自主仓库上层路径的已检入规则。

为什么 CLAUDE.local.md 没一起跳过

这是这一节最容易误解的地方。

源码并没有把 LocalProject 一起跳过,而是只对 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 应该作用于谁、作用到哪一层、在什么时机进入模型上下文”。