Claude Code 在大仓库里总改不该改的文件,多因 CLAUDE.md 缺一句边界声明 本文摘要大仓库里让 AI 修一个模块git diff 却跨了七个无关目录。把允许改动的路径写进根目录CLAUDE.md约束才能跨会话生效。一、问题与结论环境先交代macOSzshtmuxClaude Code 在仓库根目录启动内部 monorepo约 7.3 万行 TypeScript、14 个packages/*子包带根pnpm-lock.yaml和dist/。一次任务是「修packages/auth的登录超时」改完后的git status --short实际输出M packages/auth/src/login.ts M packages/auth/src/http-client.ts M packages/billing/src/invoice.ts M packages/shared/src/retry.ts M pnpm-lock.yaml M tsconfig.json ?? packages/auth/src/login.test.ts7 个变更里只有 3 个属于任务范围pnpm-lock.yaml和tsconfig.json完全不该动。另一次它自行执行了git commit提交落在我当时临时切出的实验分支上工作区还残留未跟踪文件。那轮对话里我明确说过「只改 auth」所以问题不在提示词写得不够细这句话只活在当轮会话任务一复杂或换会话就失效。根目录CLAUDE.md只有项目约定、没有边界声明约束没沉淀成项目基础设施。补一段边界声明再用.claude/settings.json的permissions.deny兜住 git 写操作改动范围才稳定收敛。二、排查与选择依据三步排查结论是约束写错了位置不是模型能力问题。翻会话历史——提示词里有边界但只在单次会话生效看根目录CLAUDE.md——只有编码约定没有「改哪些、不改哪些」看.claude/settings.json——没有permissions.deny文件编辑与 bash 默认全放行git commit、git push都能直接执行。判断依据来自官方仓库对它的定位见参考资料这是一个agentic coding toolunderstands your codebase并handling git workflows——写文件、动 git 是默认行为没有显式约束时「该动哪些」由模型自行判断。替代方案与取舍方案选择条件代价边界根目录CLAUDE.md边界声明本文单团队、路径规则稳定、要求跨会话生效每次会话都会读靠模型遵守只收窄意图不阻断执行.claude/settings.jsondeny规则需要硬阻断特定路径与 git 写命令glob 语法随版本变化路径变动要维护只挡白名单命令挡不住「该改没改」pre-commit hook CI 检查多人协作、需要机器强制CI 耗时与维护成本事后拦截文件已被改过人工git diff后合并低频、高风险仓库每次都要人工依赖审查者熟悉目录职责cc-switch 等桌面工具多个 AI 编码工具混用、需统一切换配置第三方工具配置切换与路径边界无关不限制改动范围不能替代声明不该用本文方案的两种情况任务本身就是跨包重构改packages/shared才正确边界声明会把正确改动一起拦下仓库只有几千行、目录职责一眼可见维护边界声明的成本大于收益。需要硬阻断时如 lock 文件绝对不能碰只写CLAUDE.md不够必须叠deny规则。三、关键原理CLAUDE.md是每次会话都会加载的项目上下文支持多级仓库根目录、子目录、全局~/.claude/CLAUDE.md。边界写在根目录这一级才具备跨会话的重复约束力只写在对话里的边界是一次性的。若放在子目录从packages/auth里启动会话时不一定能加载到。官方描述明确它在executing routine tasks也就是说routine例行改动的范围由模型自己划。仓库一大模型倾向于「顺手保持一致」改了packages/auth就去看packages/shared的retry.ts发现依赖没同步就动pnpm-lock.yaml。这不是幻觉是这类 AI Agent 在无边界时对「理解整个 codebase」的正常发挥——能力与风险是同一个硬币的两面。四、可运行示例环境macOS zsh、node 20一个不含真实业务代码的最小 monorepo 复现仓库。准备可直接复制mkdirdemo-repocddemo-repogitinit-qmkdir-ppackages/auth/src packages/billing/src packages/shared/srcprintfconsole.log(auth);\npackages/auth/src/index.jsprintfconsole.log(billing);\npackages/billing/src/index.jsprintfconsole.log(shared);\npackages/shared/src/util.jsprintf{private: true}\npackage.jsonprintflock-v1\npackage-lock.jsongitadd-Agitcommit-qminit实际输出准备脚本跑完后的基线$ git status --short 无输出工作区干净改前的CLAUDE.md只有约定没有边界# 项目约定 - 使用 JavaScript - 代码风格遵循 ESLint 默认配置 - 提交信息使用 conventional commits改后补上边界段第一句就是那条关键声明# 项目约定 - 使用 JavaScript - 代码风格遵循 ESLint 默认配置 - 提交信息使用 conventional commits # 修改边界优先级高于上面的约定 - 默认只允许修改 packages/auth/ 下的文件 - 禁止修改 packages/billing/、packages/shared/ 以及根目录 package.json、package-lock.json - 禁止修改 dist/、node_modules/、.github/ 下的任何文件 - 不要执行 git commit、git push、git checkout、git stash只允许 git diff、git status、git log - 不要运行 npm install、npm ci依赖由我手动更新 - 如果任务必须改到上述路径先停下并列出清单问我不要直接改 - 提交由我来做你只给建议的 commit message关键权限配置对 git 写操作做硬阻断与CLAUDE.md同目录生效cat.claude/settings.jsonEOF { permissions: { deny: [ Bash(git commit:*), Bash(git push:*), Bash(git checkout:*), Edit(package-lock.json), Edit(dist/**) ] } } EOF字段名与 glob 写法随版本变化本文未逐条验证以当前版本官方文档为准这条命令至少保证配置文件落在正确位置。跑同一个任务claude给 auth 模块加一个导出gitstatus--shortgitdiff--stat预期输出属预期未在本机复跑$ git diff --stat packages/auth/src/index.js | 1 1 file changed, 1 insertion()常见失败及其修复某次运行后git diff --stat里出现了package-lock.json。原因是边界段只写了路径、没写「不要自己装依赖」模型把同步 lock 文件当成「保持一致」的一部分。修复方式是在边界段补一行不要运行 npm install / npm ci依赖由我手动更新同时把Edit(package-lock.json)加进deny。五、验证结果与边界验证方法同一句任务在改前、改后的CLAUDE.md下各跑一次跑完立刻看git status --short与git diff --stat变更文件数从 7 降到 1–2 视为达到预期再用git log --oneline -1确认 HEAD 没被移动、当前分支没被切走。能保证的部分边界声明让每次会话都读到约束配合deny规则lock 文件写入与git push这类操作会被硬拦。不能保证的部分模型仍可能漏改该改的没改deny只挡写入、不挡「少改」边界路径写错反而会挡住正确改动。上文两次模型运行的git diff对比标注为预期、未验证只有准备脚本与基线git status是实际跑通的输出。所以提交前的git diff --stat审查不能省——边界声明负责把误改概率压低不负责替代 review。思考跨包重构任务里边界声明该临时放开路径还是干脆不写死CLAUDE.md约束与deny规则冲突时应以哪一层为准参考资料anthropics/claude-codefarion1231/cc-switch