这篇讲 CLAUDE.md 三种 scope、monorepo 下 sibling 不加载的缘由、settings 五层优先级,再附上写 CLAUDE.md 的六条经验。 这是系列文章的第8篇。
Claude 到底是怎么知道"该做什么"的?
答案藏在两种东西里:
- 记忆 (memory):CLAUDE.md、
.claude/rules/*.md、AGENTS.md------这些是写给 Claude 读的"知识" - 配置 (settings):
.claude/settings.json、.claude/settings.local.json、managed policies------这些是告诉 Claude"能做什么"的"规则"
两者都是分层的,都有优先级,都能被覆盖。搞懂这套分层,是让 Claude Code 在团队项目里稳定工作的前提。
目录概要
- 类比:入职文档 vs 公司制度
- CLAUDE.md 的三种 scope:user / project / component
- 上溯 vs 下行------为什么 monorepo 里 sibling 永远不加载
.claude/rules/*.md+ glob------比 CLAUDE.md 更精准的知识注入- settings 的 5 层优先级
- managed settings:组织级强制策略
- 数组类字段的合并规则(允许列表会叠加)
- CLAUDE.md 写作的 6 条实战经验
- 踩过的坑
- 写到这里
一、为什么要分两种:记忆 vs 配置
这俩经常被混为一谈,但其实角色完全不同------
记忆 是给 Claude 读的"背景知识 "------项目约定、命名风格、业务术语、别碰哪些目录。 配置 是给 Claude Code CLI 读的"行为开关"------用哪个模型、允许哪些工具、hooks 怎么接、MCP 连哪些服务。
用人来类比:
- CLAUDE.md ≈ 新员工入职第一天拿到的岗位说明书。项目是怎么回事、团队有哪些约定、犯忌讳的线在哪里。
- settings.json ≈ 公司制度。门禁卡开几号门、能不能用自己的电脑、报销流程走哪条。
员工读入职文档是要"理解";员工执行公司制度是要"遵守"。两者合起来才让人在公司里能正常工作。
二、CLAUDE.md 的三种 scope
先看记忆这一侧。Claude Code 支持 CLAUDE.md 写在三个层级:
| scope | 位置 | 作用范围 | 是否入 git |
|---|---|---|---|
| user | ~/.claude/CLAUDE.md |
所有项目、所有 session | 不(在你 home 下) |
| project | <repo>/CLAUDE.md |
当前仓库 | 是(团队共享) |
| local | <repo>/CLAUDE.local.md |
当前仓库 | 否(git-ignored) |
Claude Code 启动时把它能找到的全部加载进 system prompt。
三层的典型分工:
- user 级:我个人的偏好。"回答用中文"、"不要废话"、"代码注释也用中文"
- project 级 :团队约定。"本项目用 TypeScript strict mode"、"测试放在
__tests__/"、"commit 一次只改一个文件" - local 级 :我对当前项目的个人调教。"别给我跑
npm install"、"忽略 docs 目录的改动"
关键洞察:user 是你作为人带着走的偏好、project 是团队合同、local 是你和这个项目之间的"潜规则"。三者叠起来就是 Claude 在这个 session 里的全部"背景知识"。
三、上溯 vs 下行------monorepo 里 CLAUDE.md 的加载机制
这部分是 Claude Code 用户踩坑最多的地方。Boris Cherny 在 Twitter 上专门出来澄清过一次^1^,但依然容易搞错。
3.1 两种加载方向
Claude Code 启动时,对 CLAUDE.md 有两种加载方向:
- 上溯(ancestor loading) :从 cwd 一路往根目录走,遇到的每个 CLAUDE.md 都在启动时立即加载
- 下行(descendant loading) :子目录里的 CLAUDE.md 启动时不加载,只有当 Claude 读/写那个子目录的文件时才"顺便"加载进来(lazy load)
3.2 两种场景的对照
场景 1:从根跑 Claude Code
bash
cd /mymonorepo
claude
| 文件 | 启动时加载? | 为什么 |
|---|---|---|
/mymonorepo/CLAUDE.md |
✅ | 就是 cwd |
/mymonorepo/frontend/CLAUDE.md |
❌(lazy) | 子目录,读到 frontend 文件时才加载 |
/mymonorepo/backend/CLAUDE.md |
❌(lazy) | 同理 |
场景 2:从 frontend 子目录跑
bash
cd /mymonorepo/frontend
claude
| 文件 | 启动时加载? | 为什么 |
|---|---|---|
/mymonorepo/CLAUDE.md |
✅ | ancestor |
/mymonorepo/frontend/CLAUDE.md |
✅ | cwd |
/mymonorepo/backend/CLAUDE.md |
❌(永远) | sibling,不是 ancestor 也不是 descendant |
/mymonorepo/api/CLAUDE.md |
❌(永远) | 同理 |
三个关键推论------
- Ancestor 永远在启动时加载,保证"根级规则"一定生效
- Descendant 是 lazy 的,读到就加载,避免启动时爆上下文
- Sibling 永远不会加载------你在 frontend 里工作,永远看不到 backend/CLAUDE.md,设计上隔离得非常干净
3.3 这个设计解决了什么问题
想象一个 20 个子项目的 monorepo。如果所有 CLAUDE.md 都在启动时加载,上下文秒满。官方的解法是------共享约定往上走、组件特性往下走、横向不串门。
这就要求写 CLAUDE.md 时严格按层级组织:
- 根 CLAUDE.md:全仓库公共的(代码风格、commit 规范、branch 命名)
- 子 CLAUDE.md:子项目特有的(frontend 的 React 版本、backend 的 DB schema 约定)
层级清楚了,按需加载才合理。
四、.claude/rules/*.md------比 CLAUDE.md 更精准的玩法
CLAUDE.md 再怎么分子目录也粒度有限。举个例:你想说"只有改 presentation/ 下的文件时才提醒 agent 走特殊流程"------靠 CLAUDE.md 做不到。
这时候 .claude/rules/*.md 就登场了。每个 rule 文件开头写一个 glob,只在匹配到的文件被读写时才把这个 rule 注入上下文:
一个真实的 .claude/rules/presentation.md 例子:
markdown
# Glob: presentation/**
## Delegation Rule
Any request to update, modify, or fix the presentation (`presentation/index.html`)
MUST be handled by the `presentation-curator` agent...
markdown
# Glob: **/*.md
## Documentation Standards
- Keep files focused and concise --- one topic per file
- Use relative links between docs...
两种规则:一种只在碰 presentation/** 时生效,一种只在读写 .md 时生效。精度远高于 CLAUDE.md。
触发时会有 InstructionsLoaded hook(上一篇事件表里提过)fire,matcher 可以按 load_reason 筛(session_start / nested_traversal / path_glob_match / include / compact)。
对比:CLAUDE.md 是"一上来就念给 Claude 听的员工手册",rules/*.md 是"翻到那一页时才抽出来的章节"。两者互补------公共纪律写 CLAUDE.md,专题规矩写 rules。
五、settings 的 5 层优先级
现在换到配置这一侧。Claude Code 的 settings 有 5 层 ^2^,按优先级从高到低:
| 优先级 | 位置 | 作用范围 | 是否入 git | 用途 |
|---|---|---|---|---|
| 1 | managed settings | 组织 | IT 部署 | 安全策略,强制不可覆盖 |
| 2 | 命令行参数 | 单 session | --- | 临时覆盖 |
| 3 | .claude/settings.local.json |
项目 | 否(ignored) | 个人项目偏好 |
| 4 | .claude/settings.json |
项目 | 是 | 团队共享 |
| 5 | ~/.claude/settings.json |
用户 | --- | 全局个人默认值 |
低优先级被高优先级覆盖------但有个特例,稍后讲。
5.1 典型使用场景
看个具体例子。一份典型的 .claude/settings.json 可能写成:
json
{
"permissions": {
"allow": ["Edit(*)", "Write(*)", "Bash", "WebFetch(domain:*)"],
"ask": ["Bash(rm *)", "Bash(npm *)", "Bash(docker *)"]
}
}
这是团队共享的------push 到 git,每个 clone 这个项目的人都拿到一样的配置。
但我个人嫌某些规则太啰嗦,可以在 .claude/settings.local.json 里覆盖:
json
{
"permissions": {
"ask": ["Bash(rm -rf *)"]
}
}
local 优先级更高,我这台机器上 rm 不再弹确认(只对 rm -rf 弹)。但这个覆盖不会 push 到 git------队友的配置不受影响。
5.2 user 级的默认值
~/.claude/settings.json 是全局默认------所有项目都生效,除非被项目级覆盖。适合放:
json
{
"model": "sonnet",
"alwaysThinkingEnabled": true,
"language": "chinese"
}
"我在任何项目里都想用 sonnet、都想打开 thinking、都用中文回答"------放 user 级最合适。
5.3 命令行参数------临时覆盖
bash
claude --model opus
当前 session 用 opus 模型,不影响配置文件。优先级高于 settings.local.json,但低于 managed。
六、Managed settings------组织级的强制策略
第 1 层 managed settings 很特殊:任何人都不能覆盖它。即使是命令行参数也不行。
这层是给企业 IT 用的。IT 管理员能通过以下几种方式部署:
- 服务器下发(server-managed,远程)
- MDM profile (macOS plist:
com.anthropic.claudecode) - 注册表策略 (Windows:
HKLM\SOFTWARE\Policies\ClaudeCode) - 文件部署 (
managed-settings.json、managed-mcp.json) - Drop-in 目录 (
managed-settings.d/*.json,v2.1.83 新增,类似 systemd 约定)
文件路径按平台不同:
javascript
macOS: /Library/Application Support/ClaudeCode/
Linux: /etc/claude-code/
Windows: C:\Program Files\ClaudeCode\
举个场景:公司禁止员工的 Claude Code 连任何非白名单 MCP 服务。IT 部署一个 managed 配置:
json
{
"permissions": {
"deny": ["mcp__external__*"]
}
}
员工在自己项目里想添回来也没用------deny 规则永远优先 ,低优先级的 allow 覆盖不动。
6.1 drop-in 目录的合并规则
v2.1.83 新增的 managed-settings.d/ 值得一提。它沿用 systemd 的约定:
bash
managed-settings.json # base,先合并
managed-settings.d/10-telemetry.json # 后合并
managed-settings.d/20-security.json # 再后合并
managed-settings.d/.hidden.json # 忽略(以 . 开头)
合并顺序:base → drop-in 按字母序。标量字段后覆盖前;数组拼接 + 去重 ;对象深合并。
为什么这么设计?------不同小组写不同规则文件不打架。安全组写 20-security.json、埋点组写 10-telemetry.json,各管各的。
七、数组字段的合并------不是覆盖,是拼接
上一节最后提到一个关键点:数组类设置跨层级是拼接合并,不是替换。
比如:
- user
~/.claude/settings.json:"allow": ["WebSearch"] - project
.claude/settings.json:"allow": ["Bash", "Edit"] - local
.claude/settings.local.json:"allow": ["Write"]
最终 Claude Code 生效的 allow 列表是:
css
["WebSearch", "Bash", "Edit", "Write"]
全部拼接、去重,不是 local 覆盖 project 覆盖 user。
这个设计很符合直觉:权限列表是"加白名单"的动作,合并是叠加而不是覆盖。想像一下如果每一层都是覆盖------每个开发者的 local 要重新抄一遍 project 的所有 allow,才能加一条自己的。这显然很蠢。
但另一个字段行为不同:
deny规则有最高安全优先级,不能被低优先级的 allow/ask 覆盖。
也就是说,如果 managed 里写了 deny: ["Bash(rm -rf /)"],你无论在哪一层 allow 都没用。deny 是全局安全底线。
八、CLAUDE.md 的写作经验
讲完机制,讲点实操。我自己维护 CLAUDE.md 经历过多次迭代,总结了 6 条经验:
8.1 一个文件 200 行以内
这是反复试出来的经验值。超过 200 行,Claude 对前半段的遵守度明显下降------注意力是有限的。
如果内容超过 200 行,拆:
bash
CLAUDE.md # 纲领性说明 + 指路
.claude/rules/testing.md # 测试相关约定
.claude/rules/commit.md # commit 规范
.claude/rules/i18n.md # 国际化约定
8.2 "做什么" 比 "不做什么" 更有用
❌ "不要使用任何外部库" ✅ "依赖必须走 pnpm add,不要改 package.json"
负面指令告诉 Claude"什么是错的",但不告诉它"什么是对的"。正面指令既说了规则又说了做法。
8.3 用代码块展示规范
❌ "commit message 要简洁有意义" ✅ ```格式:: ,subject 用祈使语气。示例:feat: 添加用户头像上传、`fix: 修复登录表单验证````
规则越具体,遵守度越高。能给例子就给例子。
8.4 开头定位,不要写目录
❌
markdown
# 目录
1. 项目概述
2. 编码规范
...
✅
markdown
# CLAUDE.md
This file provides guidance to Claude Code when working with code in this repository.
## Repository Overview
...
Claude 不需要 TOC,它会一口气读完。开头直接讲 repo 是什么、核心模块有哪些,比目录有用。
8.5 "git commit rules" 这种高频动作要专门列出来
我自己的 CLAUDE.md 里专门有一节:
markdown
## Git Commit Rules
When committing changes, **create separate commits per file**.
Do NOT bundle multiple file changes into a single commit.
为什么单列?因为这是每次都要做、做错就麻烦的动作。写在 CLAUDE.md 里比挂 hook 简单,效果也足够好。
8.6 别和 hooks 功能重复
如果你已经有个 PreToolUse hook 会拦截危险 Bash 命令,那 CLAUDE.md 里就不用再啰嗦"不要运行危险命令"。
心智区分 :CLAUDE.md 是知识 ,hooks 是强制。能用 hooks 强制执行的,不要再写进 CLAUDE.md------浪费 context,还容易冲突。
九、踩过的坑
9.1 CLAUDE.md 写太长,后半段不被遵守
第一次写 CLAUDE.md 我写了 500+ 行,结果项目运行时 Claude 完全不记得里面某些章节的规则。拆成 CLAUDE.md + .claude/rules/*.md 之后明显变好。200 行。
9.2 local vs project 配置混淆
.claude/settings.local.json 是 git-ignored 的个人覆盖------结果有人把团队 hook 配置写在这里,同步不到 git,其他队友根本拿不到。
记住 :想让队友拿到的配置 → settings.json;只在自己机器上用的 → settings.local.json。
9.3 sibling CLAUDE.md 不会加载
在 /monorepo/frontend/ 下跑 Claude Code,期待 /monorepo/backend/CLAUDE.md 里的约定也生效------不会的,sibling 永远不加载。
解决办法:把跨项目约定上提到根 CLAUDE.md,两个 sibling 共享。
9.4 managed settings 不能本地覆盖
有人在企业环境里发现自己的 settings.local.json 改了但没效果------没用 managed 在上面压着。disableAllHooks 在 local 里设也不能关掉 managed 部署的 hook(这是 v2.1.49 之后的官方行为)。
9.5 数组合并而不是替换,容易出现意外 allow
我曾经想"清空 allow 只保留一条",在 local 里写 "allow": ["WebSearch"] ------ 结果 project 和 user 里的 allow 还在,merge 出一个比我想的更宽的列表。
想删某条规则,不能 靠覆盖;数组是加法,不是替换。要么直接改上层文件,要么把要禁的写进 deny。
十、零件齐活了
memory 和 settings 这对分层系统,是 Claude Code 最底层的"它知道什么 "和"它能做什么"。
复盘一下------
- memory 管"知识":CLAUDE.md 三层(user/project/local),monorepo 里上溯到根、下行 lazy 加载、sibling 不串门。想精确到某类文件用
.claude/rules/*.md+ glob。 - settings 管"规则":5 层优先级(managed > CLI > local > project > user),数组字段合并而非覆盖,deny 规则有全局最高优先级。企业场景用 managed 部署不可绕过的策略。
- 分工:知识写 CLAUDE.md、强制走 hooks/settings、专题放 rules------别让三者职能重叠。
这 8 篇串起来,Claude Code 作为单机工具的内部机械基本拆完了:Commands(按钮)、Skills(工具)、Subagents(员工)、Hooks(挂钩点)、Memory(入职文档)、Settings(公司制度)。
零件都认完了,下一步是让它们一起跑起来。
03 篇里跑过一遍 release-notes 生成器------一个 /release-notes-crafter 带起一个 subagent、agent 预加载一个 skill 先跑 git log、再直接调另一个 skill 排版,最后写出 RELEASE_NOTES.md。那篇讲的是"怎么执行"。下一篇要讲的是"为什么这么编排"------Command / Agent / Skill 三者在一个工作流里是怎么分工的?能不能把 agent 内嵌另一个 agent?preload 的 skill 跟 Skill tool 调出来的 skill 有什么本质区别?同一件事"打开时间",在 command、agent、skill 三种实现里,Claude 最后会选哪一个?这些问题 03 篇只说了一半,下一篇来把它讲透。
参考资料
外部链接
- Humanlayer - Writing a good Claude.md --- 一篇经常被引用的 CLAUDE.md 写作指南
- Claude Code Memory Documentation --- memory lookup 的官方文档
Footnotes
-
Boris Cherny on X - CLAUDE.md Loading Clarification --- 官方对 ancestor / descendant 加载机制的澄清 ↩
-
Claude Code Settings Reference --- 官方 settings 层级与全部字段 ↩