教程Claude Code系列第 6 / 6 篇

大仓库和 monorepo 里用 Claude Code:让它只看手上这块代码

仓库一大,Claude Code 的上下文容易被无关的约定和文件占满。本站在一个三个包的示例仓库上实测了官方的几样办法:分层的 CLAUDE.md、排除不相干的包、拒绝读第三方代码、追加兄弟目录、稀疏 worktree。要留意:拒绝读取的规则挡不住 grep -r;auto 模式下它会改启动目录以外的文件。

发布 · 约 13 分钟读完 · 经人工审核()· 本站的内容怎么来的

本站已实测,测试日期:2026-10-12。

本页目录(12 节)
  1. 示例仓库
  2. 第一个决定:从哪里启动
  3. 排除不相干的包:claudeMdExcludes
  4. 不让它读某些文件:Read 拒绝规则
  5. 从子目录启动,又要动别的包
  6. 只检出要用的目录:稀疏 worktree
  7. 每个包自己的 skill
  8. 分层管不住了怎么办
  9. 一个改动跨好几个包的时候
  10. 这一趟花了多少
  11. 这次实测没有覆盖到的
  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 模式下分类器放不放行,取决于你怎么说、它怎么判断,不要把这次的“放行”当成固定行为。

下一步

来源

通过 API 中转站使用这些模型?到 Veridrop 免费检测中转站给你的是不是它声称的模型,或者看看中转站排行榜前 100 名。

相关文章