大仓库和 monorepo 里用 Claude Code:让它只看手上这块代码
仓库一大,Claude Code 的上下文容易被无关的约定和文件占满。本站在一个三个包的示例仓库上实测了官方的几样办法:分层的 CLAUDE.md、排除不相干的包、拒绝读第三方代码、追加兄弟目录、稀疏 worktree。要留意:拒绝读取的规则挡不住 grep -r;auto 模式下它会改启动目录以外的文件。
本站已实测,测试日期:2026-10-12。
本页目录(12 节)
这篇教程解决一个问题:仓库很大,或者一个仓库里有很多个包,怎么让 Claude Code 把注意力放在你手上这一块,而不是把整个仓库的规矩和文件都装进上下文。
Claude Code 在多大的仓库上都能跑。但仓库一大,为小项目调好的默认做法会出两个问题:一份根目录的 CLAUDE.md 要么越写越长,把每个子系统的约定都塞进去;要么写得太泛,没什么用。再加上它找东西时读进来的文件,上下文很快被和当前任务无关的内容占掉,既费 token,也让它分心。
官方文档给了一组互相独立的设置,按需要叠加。本站建了一个小的 monorepo,把其中几样逐个验证了一遍。
| 实测环境 | |
|---|---|
| 日期 | 2026-10-12 |
| 版本 | Claude Code 2.1.296 |
| 系统 | Ubuntu 24.04 |
| 模型 | Opus 5.5 |
| 连接方式 | 通过酷站AI 中转站,不是直连官方 |
| 示例仓库 | 自己建的一个三个包的小 monorepo,见下 |
示例仓库
b6-monorepo/
CLAUDE.md 全仓库的约定
packages/
api/
CLAUDE.md api 包的约定
.claude/skills/api-testing/ api 包的一个 skill
src/routes.js
vendor/payments-sdk/ 随仓库提交的第三方代码
web/
CLAUDE.md web 包的约定
src/Button.js
src/client.js
shared/
CLAUDE.md shared 包的约定
src/money.js
为了能验证“某份约定到底加载了没有”,每份 CLAUDE.md 里放的都是容易辨认的规矩。根目录写的是全仓库通用的:
- 包里的脚本在包自己的目录下运行,不在仓库根目录运行。
- 提交说明以包名开头,例如 `api: add rate limiting`。
- 不要改 `packages/*/vendor/` 下的文件,那是别人的代码。
packages/api/CLAUDE.md 写的是这个包自己的:
- 数据库查询一律用 Knex 的查询构造器,不在路由处理函数里写 SQL 字符串。
- 接口返回的时间一律用 ISO 8601 字符串,不返回时间戳数字。
- 合并过的迁移文件不能再改,要改就加一个新的迁移。
web 包和 shared 包各有三条自己的。验证的办法是:不许它读文件,直接问某个包的规矩,看它答不答得出。
这也是官方推荐的分法:根目录放处处适用的规矩,每个包或子系统放自己技术栈的约定,随代码一起提交,由各自的负责人维护。
第一个决定:从哪里启动
在哪个目录下运行 claude,决定了三件事。表来自官方文档:
| 从哪启动 | 启动时加载的 CLAUDE.md | 适合 |
|---|---|---|
| 仓库根目录 | 只有根目录那份;子目录的用到时才加载 | 任务跨几个包 |
| 某个子目录 | 这个目录的,加上往上每一级的 | 只在一个包里干活 |
两种我们都试了。
从根目录启动
刚启动时看 /context,记忆文件只有一份:
Memory files · /memory
└ CLAUDE.md: 240 tokens
这时问 api 包的规矩,不许读文件:
❯ 不要读任何文件、不要跑命令。api 包里数据库查询有什么规定?不知道就只回答三个字:不知道。
● 不知道
然后让它读 api 包里的一个文件:
❯ 用 Read 工具读 packages/api/src/routes.js,只告诉我它导出了哪几个名字。
Read 1 file (ctrl+o to expand)
⎿ Loaded packages/api/CLAUDE.md
⎿ Loaded 1 skill from packages/api/.claude/skills
⎿ 1 skill available
读文件的同时,这个包的 CLAUDE.md 和 skill 被带了进来。再问同一个问题,仍然不许读文件:
● 根据 packages/api/CLAUDE.md:
- 数据库查询: 一律用 Knex 的查询构造器,不在路由处理函数里写 SQL 字符串。
- 接口返回的时间: 一律用 ISO 8601 字符串,不返回时间戳数字。
而 web 包的文件它没碰过,问 web 的规矩,回答还是“不知道”。
有一点要留意:这时再看 /context,记忆文件那一栏仍然只列着根目录的一份。用到时才加载的那些不出现在这个清单里,官方文档也是这么说的。要确认某个子目录的 CLAUDE.md 加载了没有,看读文件时有没有 Loaded ... 那一行。
从子目录启动
cd packages/api
claude
/context 里一开始就是两份,skill 也已经在了:
Memory files · /memory
├ ~/demo/b6-monorepo/CLAUDE.md: 240 tokens
└ CLAUDE.md: 186 tokens
Skills · /skills
Project
└ api-testing: < 20 tokens
一句话问三件事,不许读文件:
● 1)api 包里的数据库查询:一律用 Knex 的查询构造器,不要在路由处理函数里写 SQL 字符串。
这条写在 packages/api/CLAUDE.md 里。
2)提交说明的格式:以包名开头,例如 api: add rate limiting。这条写在仓库根目录的 CLAUDE.md 里。
3)web 包里样式的规定:不知道。现在加载的只有根目录和 api 包的 CLAUDE.md,两份都没提到
web 包的样式。
本包的和全仓库的都在,别的包的不在。只在一个包里干活的时候,这是最省事的办法:什么都不用配,换个目录启动就行。
还有一点容易想当然:项目设置(.claude/settings.json)不像 CLAUDE.md 那样从上级目录继承。 从哪个目录启动,读的就是哪个目录的。从子目录启动的团队,每个包的 .claude/settings.json 要各自写全。
CLAUDE.md 还是 .claude/rules/
两种都能把约定限定在一部分代码上。官方文档的区分:
| 做法 | 文件在哪 | 什么时候加载 | 适合 |
|---|---|---|---|
| 每个目录一份 CLAUDE.md | 和代码放在一起 | 从这个目录启动时,或者用到时 | 各目录的负责人自己维护 |
.claude/rules/ 里带 paths 的规则 |
集中在仓库根目录的 .claude/ |
碰到匹配路径的文件时 | 想把约定集中放,或者同一条规则适用于很多分散的路径 |
规则文件的写法见CLAUDE.md 怎么写。
排除不相干的包:claudeMdExcludes
从根目录启动时,碰到哪个包的文件,哪个包的 CLAUDE.md 就会加载。有些包你从来不管,可以让它们的约定永远不加载。
只想对自己生效,写在 .claude/settings.local.json 里:
{
"claudeMdExcludes": [
"**/packages/web/**"
]
}
我们做了对照。同一个问题:先让它读 web 包里一个文件的第一行,然后问 web 包请求接口的规定。
没有排除规则时:
规定是:web 包里请求接口一律通过 `src/client.js`,组件里不能直接调用 `fetch`。
这条写在 `packages/web/CLAUDE.md` 里。我读 `client.js` 第一行时,这份文件被自动加载进来了。
加上排除规则之后:
不知道。
`packages/web/src/client.js` 的第一行是 `export async function getOrders() {`,只说明这里定义了
一个获取订单的异步函数,看不出请求接口有什么规定。……读这个文件时也没有出现 web 包自己的说明文件。
几点说明,来自官方文档:
- 模式是 glob,匹配的是文件的绝对路径,所以相对写法要以
**/开头。 - 它会跳过这个路径下所有的 CLAUDE.md 和规则文件。
- 这是一份固定的清单,不是按任务切换的开关。 今天做这个包、明天做那个包,应该换启动目录,不要来回改排除规则。
- 自己手动建
.claude/settings.local.json的话,记得把它加进.gitignore。
不让它读某些文件:Read 拒绝规则
它的内容搜索默认遵守 .gitignore,所以 node_modules/、dist/ 这些本来就不会出现在搜索结果里。要另外处理的是提交进了仓库的那些:打包进来的第三方 SDK、提交了的生成代码。
在 .claude/settings.json 里加拒绝规则:
{
"permissions": {
"deny": [
"Read(./**/vendor/**/*)"
]
}
}
我们让它用四种办法去读同一个文件,并要求如实汇报、不要绕:
四步里前三步被拒,第四步成功了:
1. Read 读 `packages/api/vendor/payments-sdk/index.js`:被拒绝。返回的是
"File is in a directory that is denied by your permission settings"。
2. `ls packages/api/vendor/payments-sdk`:被拒绝。这条 Bash 命令没有获得权限。
3. `cat packages/api/vendor/payments-sdk/index.js`:被拒绝。同样没有获得权限。
4. `grep -rn version packages/api/vendor`:成功。输出如下:
packages/api/vendor/payments-sdk/index.js:2:module.exports = { version: '4.2.1', charge: ...
返回的 permission_denials 里正好是前三条。
第四条是关键。拒绝规则挡得住 Read 工具和 cat 这类直接点名文件的命令,挡不住 grep -r 这种对整个目录的搜索,文件的内容照样出现在输出里。这和官方文档写的一致:拒绝规则覆盖内置的文件工具,以及路径作为参数出现的 cat、head、grep、find 等命令;对一个包含被拒绝文件的目录做 grep -r 或 find,结果里仍然会有它们。命令自己再启动的子进程去读文件,也不在覆盖范围内。
所以这条规则的用处是省上下文:让它不去翻几万行的第三方代码。它不是保密手段。真不能让它看到的东西,不要放在它够得着的地方。
规则放在哪个文件,决定了对谁生效:提交进 .claude/settings.json 是所有人;.claude/settings.local.json 是只有你;托管设置是强制所有会话。
从子目录启动,又要动别的包
从 packages/api/ 启动,任务却要改 packages/shared/ 里的一个共用的东西。这时要把兄弟目录加进来。
启动时加:
claude --add-dir ../shared
或者写进这个包的 .claude/settings.json:
{
"permissions": {
"additionalDirectories": ["../shared", "../web"]
}
}
不加会怎样,取决于权限模式
这一点我们测出来的结果比预想的复杂。同一个要求:从 packages/api/ 启动,把 ../shared/src/money.js 里的一个常量改掉。
| 权限模式 | 没有 --add-dir |
有 --add-dir ../shared |
|---|---|---|
acceptEdits |
没改成,读那个文件的请求被拒绝 | 改成了 |
| auto(交互式会话的默认) | 改成了,显示 Allowed by auto mode classifier |
— |
dontAsk 加 --allowedTools "Read" "Edit" |
改成了,没有任何拒绝 | — |
acceptEdits 那一次的回答:
没改成。读取 `../shared/src/money.js` 的权限申请没有被批准,而 Edit 工具要求先读过文件才能修改,
所以我没能调用 Edit。
也就是说,“从子目录启动,它就只能动这个子目录”这句话只在一部分情况下成立:
- auto 模式下,启动目录不是一道硬边界。 你明确要求它改兄弟包的文件,分类器放行了。
- 允许列表里写一个不带路径的
Edit,等于允许改任何地方。 要限定范围,写成带路径的规则。 acceptEdits和 Manual 模式下,边界才起作用:目录以外的读写会来问你,没人回答就被拒绝。
如果你靠“从子目录启动”来防止它改到别的包,先确认自己用的是哪种权限模式。几种模式的区别见权限模式怎么选。
加进来的目录,约定不会自动跟来
加了目录,它能读能改那里的文件。那个目录的 CLAUDE.md 加不加载,是另一回事。官方文档的表:
| 怎么加的 | 加载 CLAUDE.md 和规则 | 加载 skill |
|---|---|---|
additionalDirectories 设置 |
不加载 | 不加载 |
--add-dir 参数或 /add-dir 命令 |
设了下面的环境变量才加载 | 加载 |
我们验证了 --add-dir 这一行。只加目录,问 shared 包的金额单位:
$ claude -p "不要读任何文件、不要跑命令。shared 包里金额用什么单位?……" --add-dir ../shared
不知道
加上环境变量:
$ CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 claude -p "……" --add-dir ../shared
shared 包里的金额一律用“分”做单位,存成整数,不用小数。
要改一个包,却不知道那个包的规矩,改出来的东西多半不合那边的约定。跨包干活时,记得把这个变量带上。
只检出要用的目录:稀疏 worktree
claude --worktree 在一个新的 git worktree 里开会话,默认检出整个仓库。仓库很大时,可以只检出列出的目录。
在仓库根目录的 .claude/settings.json 里加:
{
"worktree": {
"sparsePaths": [
".claude",
"packages/api",
"packages/shared"
]
}
}
然后:
claude --worktree sparse-demo
新 worktree 里的内容:
$ ls -a .claude/worktrees/sparse-demo
.claude .git .gitignore CLAUDE.md package.json packages
$ ls .claude/worktrees/sparse-demo/packages
api shared
主目录里有 api、shared、web 三个包,worktree 里只有列出的两个。根目录下的文件(CLAUDE.md、package.json)都在。
几条规矩,来自官方文档,前两条我们的结果与之相符:
- 列目录,不列单个文件。 根目录下的文件总是会检出。
- 根目录下的目录不会自动检出。 所以清单里要有
.claude,不然 worktree 里就没有仓库的设置和规则了。 - 路径相对于仓库根目录,和你从哪个子目录启动无关。
- 一个会话里所有的 worktree 共用同一份清单。两个子代理一个要 api、一个要 web,就两个都列上。
文档还提到一个实现细节,我们也看到了:有稀疏 worktree 存在时,仓库共享的 git 配置里会多出 extensions.worktreeConfig = true。文档说最后一个 worktree 删除后,Claude Code 会把它自己加的这一项去掉。
想避免每个 worktree 都复制一份 node_modules,在同一处加 "symlinkDirectories": ["node_modules"],让它们链接回主目录的那份。这个我们没有试。worktree 的基本用法见用 worktree 同时开两个会话。
每个包自己的 skill
skill 可以跟着包走,放在包里的 .claude/skills/ 下。它只在相关时才加载,所以 api 包的测试写法不会在做前端时占上下文。
前面已经看到两种情况:从 packages/api/ 启动,skill 一开始就在;从根目录启动,碰到 api 包的文件时才被发现(Loaded 1 skill from packages/api/.claude/skills)。
官方文档提醒的一个问题是数量。从根目录启动的长会话,碰过的包越多,累积的 skill 越多,可以到几百个。名字总会加载,但数量多了之后一部分 skill 的说明会被整个省掉,它判断该不该用就少了依据。对策:
- 说明写短,把请求里会出现的词放在最前面,比如“在 packages/api/ 下新增或修改测试时使用”。
- 很多包共用的 skill(PR 的规矩、发布清单)放在仓库根目录的
.claude/skills/。 - 需要单独的版本历史、或者要跨仓库用的,打包成插件。
分层管不住了怎么办
每个目录一份 CLAUDE.md,包多了以后会出现另一种问题:约定各写各的,文件过时了没人管,根目录那份没人负责。
官方的建议是把约定和参考资料从“总是加载”的 CLAUDE.md 里挪出来,放进用到才加载的地方:
- skill:只在和任务相关时加载的参考资料。
- 插件:把 skill、hook、命令打成有版本的一包,由一个团队集中维护。
- MCP 服务器:组织里如果已经有代码搜索或者索引服务,把它做成一个 MCP 工具,让它去查,而不是自己读文件。
这几样各管什么,见CLAUDE.md、skill、hook、子代理、MCP、插件各管什么。
一个改动跨好几个包的时候
上面的设置管的是它看得到什么。一个改动要动好几个包(比如改一个共用的类型,再改掉所有用到它的地方)时,官方还给了两条做法上的建议:
- 整件事放在一个会话里做。 共用的那处修改和所有调用的地方一起交给它,每一处的决定才能前后一致。
- 先出计划。 在 plan 模式下让它把计划写出来。跨包的长会话中途会压缩上下文,而计划文件在每次压缩后都会重新读入,对话历史可能丢,计划不会。
这两条的实际做法见重构和迁移。
这一趟花了多少
三个交互式会话加十几次 claude -p,合计约 2.5 美元,是 Claude Code 按官方价格估算的数字,不是中转站实际扣的金额。其中 -p 的调用占了大半,每次哪怕只回一个“不知道”,估算也在 0.15 美元上下,原因见把 Claude Code 接进脚本和 CI里关于固定开销的部分。
这次实测没有覆盖到的
- 示例仓库很小,三个包、十来个文件。“省了多少上下文”在这个规模上看不出来,这篇验证的是每样设置起不起作用,不是效果有多大。
- 代码智能插件没有试。 它要在机器上装对应语言的语言服务器。官方文档说它能让 Claude 直接跳到定义、查引用,不用靠读文件和 grep 去找。
additionalDirectories、symlinkDirectories、带paths的规则没有在这篇里试。- Manual 模式下跨目录读写的确认框没有截到。 auto 模式下读兄弟包的文件时,屏幕上闪过一个标题为
Read outside the working directories的确认框,随后自动通过了,我们没有来得及完整记下它的选项。 - 托管设置、组织级的部署没有涉及。
- 每种情况只跑了一次。 auto 模式下分类器放不放行,取决于你怎么说、它怎么判断,不要把这次的“放行”当成固定行为。
下一步
- 把一组约定和 skill 打成插件分发:做一个插件,分享给团队。
- 几个包同时改,各开一个会话:用 worktree 同时开两个会话。
- 上下文里到底装了什么、怎么看:会话和上下文。