你让 Claude Code 改 billing.ts 里一个函数名。Diff 回来 12 个文件、200 行:4 个不相关模块的”一致性 pass”、你没要的格式改动、8 个文件 import 顺序的”小改进”。原任务 5 分钟,现在 review 要 30 分钟。
最快的修法: 别只靠 prompt。在 permissions.deny 里加一条规则,让 Claude Code 在物理层面就动不了目标之外的文件;再给每个 prompt 一份明确的可编辑文件清单,把发现的相邻问题写进 FOLLOWUPS.md 而不是顺手改掉。没有边界时,Claude 会沿着它认为”相关”的代码路径走:import 引向 caller,caller 引向其他模块,模块各有不一致——一个文件的活儿就变成了重构。
进入步骤前先记住一件事:permissions 规则是 Claude Code 这个 harness 强制执行的,CLAUDE.md 里的指令不是。官方文档说得很直白:“Permission rules are enforced by Claude Code, not by the model. Instructions in your prompt or CLAUDE.md shape what Claude tries to do, but they don’t change what Claude Code allows.”所以 prompt 层的围栏(Step 2-3)只是降低它扩张的冲动;真正能拦住一次写入的,只有 permissions deny 规则(Step 1)。
你属于哪一种?
| diff 里的症状 | 最可能的原因 | 跳到 |
|---|---|---|
| 改名出现在你从没提过的文件里 | prompt 没指文件边界 | Step 1、原因 1 |
| 一处核心改动 + 一堆 call site 小改 | agent 沿 import / call 链扩散 | 原因 2 |
| 和任务无关的 import 顺序 / 格式 / 命名改动 | 自发的一致性 pass | 原因 3 |
大范围改动”刚好符合”某条 CLAUDE.md 规则 | 常设的”顺手清理”授权 | Step 3、原因 4 |
| 整个 diff 确实都是必要的 | 真·多文件重构塞进一个 PR | Step 6、原因 5 |
| 跟着改名改了一大堆测试文件 | ”修测试”级联 | Step 7、原因 6 |
常见原因
按命中率从高到低。
1. Prompt 没指文件边界
「把 getUser 改名 findUser」没说在哪儿。Claude 全局改名——包括你正在迁移走的 legacy 路径,那里改名反而把迁移破坏了。
如何判断:回看 prompt——没列具体文件,边界就是隐式的,Claude 会自由扩。
2. Agent 沿 import / call 链扩到邻居文件
Claude 在 service.ts 改了名,然后更新了全部 8 个 call site。每一处都是合法触达,但累计起来是你预期的 9 倍。
如何判断:diff 里有一处「核心」文件改动 + 很多 call site 小更新。每条都必要,合起来就是扩 scope。
3. Agent 自发做了一致性 pass
Claude 注意到一半文件用 import type、一半没用,改名时顺便”协调”了 6 个文件——一句没问。
如何判断:diff 里有和改名无关的改动(import 顺序、格式、命名)——纯 creep。
4. CLAUDE.md / prompt 授权了「顺手清理」
# CLAUDE.md
- 看到机会就改进代码质量
- 相关工作中顺手修明显问题
看着无害的规则,其实是 scope 扩张的常设授权。
如何判断:grep -i "improve\|clean up\|while you're\|opportunit" CLAUDE.md——每条匹配都是一条 creep 授权。
5. 重构本质就跨多文件
某些重构真的跨整个 codebase(改公开 API、迁 state 库)。scope 是真的——把它 bundle 进一个 PR 才是问题,应该拆。Anthropic 自己的建议是:单次会话可靠的上限大约 20-40 个文件;超过就拆成多个阶段,每阶段之间 commit 一次。
如何判断:diff 本身真的必要——bug 是「一个巨型 PR」而不是「PR 链」。
6. Agent “修测试” 顺势级联
核心改动后测试挂了,Claude 在多个测试文件里更新 expectation、mock、fixture。有些合法(匹配新签名),有些是在 mask 失败。
如何判断:测试文件改动跟着改名一起来——审一下哪些是签名更新、哪些是悄悄削弱断言。
最短修复路径
按收益从高到低。Step 1 是唯一的硬强制;Step 2-3 在 prompt 层塑造行为。
Step 1:用 permissions deny 规则强制边界
这正是当初那个巨型 diff 缺的东西。在 .claude/settings.local.json 里加一条 Edit deny 规则,让 Claude Code 拦住任何目标之外的写入,无论 prompt 或 CLAUDE.md 怎么说。Edit 规则对所有内置的文件编辑工具、以及 Claude Code 识别得出的写文件 Bash 命令(sed 等)都生效。
{
"permissions": {
"deny": [
"Edit(/src/**)"
],
"allow": [
"Edit(/src/services/user.ts)",
"Edit(/src/services/user.test.ts)"
]
}
}
规则按 deny、ask、allow 的顺序求值,所以宽泛的 Edit(/src/**) deny 只会被那两条明确的 allow 路径覆盖。路径用 gitignore 语义:开头的 / 指项目根,Edit(/src/**) 就是 <project>/src/**。(注意 /Users/... 这种不是绝对路径;绝对路径要双斜杠,比如 Edit(//tmp/scratch.txt)。)任何时候都能用 /permissions 查看当前生效的全部规则、以及每条来自哪个 settings 文件。
如果你更想默认禁掉所有编辑、只放开你列出的文件,用 dontAsk 模式跑这次会话(除非已通过 permissions.allow 预批,否则自动拒绝):
claude --permission-mode dontAsk
对于那些永远不该被随手改的文件(lockfile、生成的代码、CI 配置),在签入版本库的 .claude/settings.json 里常驻一条 deny 规则:
{
"permissions": {
"deny": [
"Edit(/**/*.lock)",
"Edit(/dist/**)",
"Edit(/.github/workflows/**)"
]
}
}
Step 2:在 prompt 里钉死可编辑文件清单
deny 规则拦的是写入;prompt 清单让 Claude 一开始就不去尝试(也省得它把 transcript 塞满被拦住的重试)。
可编辑文件(其他一律不许动):
- src/services/user.ts
- src/services/user.test.ts
任何其他文件:
- 必须动就停下来问,再编辑。
- 任何地方都不要做一致性 / 格式 / 清理改动。
「停下来问」这一句让 Claude 能 flag 出真正的级联,而不是私自扩张。
Step 3:相邻问题导向 FOLLOWUPS.md
在可编辑清单外的文件发现问题时:
- 不要修。
- 在 `FOLLOWUPS.md` 加一行,写清问题 + file:line。
- 这些变成 future task,不进本 PR。
这样 Claude 仍能”帮上忙”但不扩 scope——每个发现都成为文档化的 future task。配合一次 CLAUDE.md 审查:
grep -in "improve\|clean up\|while you're there\|opportunistically" \
CLAUDE.md src/**/CLAUDE.md prompts/*.md
把每条松散授权换成更紧的:
- 当前任务 scope 之外的不要动。
- 相邻问题进 FOLLOWUPS.md,不进当前 PR。
Step 4:先 plan,让 scope 在动手前就可见
任何可能级联的活儿,先进 plan 模式:Claude 可以读文件、跑只读命令,但在你批准计划前动不了源码。按 Shift+Tab 循环切换权限模式(依次经过 default、accept-edits、再到 plan;底部状态栏显示当前模式),或直接 claude --permission-mode plan 启动。这样你能看到它打算改哪些文件,并在任何一次写入之前把范围缩小。批准计划后执行,或继续打磨。
截至 2026 年 6 月有一个已知的小坑:在没批准计划的情况下用 Shift+Tab 循环退出 plan 模式,可能注入一条 “work without stopping for clarifying questions” 的提醒(anthropics/claude-code#59203)。要放弃一个计划,明确清掉它,而不是用 tab 循环绕过去。
Step 5:收到过宽的 diff,回退 scope 外的文件
不要原样 merge,也不要全盘拒绝——只回退 scope 外的那些改动。
整个文件不该被动时,整文件回退:
git diff --stat HEAD # 看所有改动文件
git checkout HEAD -- <scope 外文件> # 把它们还原到 HEAD
同一个文件里既有想要的、又有不想要的改动时,按 hunk 回退:
git checkout -p src/services/user.ts
# 对每个 scope 外的 hunk 回答 "y" 丢弃,对真正的改动回答 "n" 保留
然后确认核心改动还能过:
pnpm test
如果过宽的改动是在当前未提交的会话里发生的,/rewind(Claude Code 基于 checkpoint 的撤销,v2 起可用)一条命令把代码和对话一起回滚到更早的点。优先用它,别用 git reset --hard——后者是破坏性的,会丢掉你可能还想要的未提交改动。
Step 6:真·多文件重构就拆 PR 链
如果改名真的要更新 8 个 caller,就阶段化发布:
PR 1:加 `findUser`,与 `getUser` 共存(不动 caller)。
PR 2:迁 `src/services/billing/` 的 3 个 caller。
PR 3:迁 `src/api/` 的 5 个 caller。
PR 4:删 `getUser`。
每个 PR 5 分钟 review。总 diff 一样,但可 review 性高 10 倍。
Step 7:核查测试有没有被削弱
diff 里的每个测试文件:
git diff -- src/services/user.test.ts
# 标记:被删的 expect/assert、新加的 .skip、放宽的类型检查
git diff -- src/services/user.test.ts | grep -E '^-.*expect|^-.*assert|^\+.*skip'
如果断言被删、测试被 skip,那”修测试”其实是把回归伪装成了修复。还原原测试,重新 prompt 让它去修生产代码。
怎么确认修好了
git diff --stat HEAD列出的只有你打算改的文件(外加它们的测试)。/permissions显示Editdeny 规则已生效;重跑任务时,对任何 scope 外文件会给出被拦住的提示,而不是悄悄写入。- 对测试 diff 的 grep 没有任何输出(没删断言,没新增
.skip)。 FOLLOWUPS.md多了几行——Claude 注意到、但没动手改的相邻问题。
预防建议
- 对永远不该随手改的文件(lockfile、生成代码、CI 配置)常驻一条签入的
Editdeny 规则;强制执行胜过好意。 - 每个代码 prompt 都给明确的可编辑文件清单,越界就「停下来问」。
- 容易级联的任务在 plan 模式里跑,让目标文件集在任何写入前就可见。
- 维护一个
FOLLOWUPS.md,让 Claude 能浮出相邻问题而不动手。 - 审
CLAUDE.md,删掉「顺手改进」这类常设授权。 - 真的多文件重构拆成 PR 链,别一个巨型 PR;单次会话可靠上限按 ~20-40 个文件估。
- 抽查测试 diff——删断言 / 加 skip 是伪装成修法的 creep。
- review 时对照原始 scope,而不只是看最终 diff——diff 本身可能看着挺合理。
常见问题
我 prompt 里明明写了”只改 billing.ts”,为什么 Claude Code 不听?
因为 prompt 指令不是边界。模型把它当成参考,一旦判断某个相关文件”需要”改,就会越过去。只有 permissions.deny 规则(Step 1)是 harness 强制执行的。把规则放进 .claude/settings.local.json,prompt 指令就成了一道硬墙上面的提示。
限制 scope 该用 deny 还是 plan 模式? 两个都用,分工不同。plan 模式在你批准计划前禁掉所有编辑,适合先把 scope 摸清楚。deny 规则允许编辑、但拦住特定路径,适合在你已经确定哪些文件在范围内之后。plan 模式用于探查,deny 规则用于执行。
怎么让 Claude 只改一个文件、把 src/ 其余都挡住?
宽 deny 配窄 allow,记住 deny 先求值:"deny": ["Edit(/src/**)"] 加 "allow": ["Edit(/src/services/user.ts)"]。开头单斜杠,所以路径是相对项目根的。
Claude 已经搞出了 12 个文件的烂摊子,怎么只留我想要的那部分最安全?
别 git reset --hard。整个 scope 外文件用 git checkout HEAD -- <文件> 回退,混合文件里逐个 hunk 用 git checkout -p <文件> 丢弃。如果改动还在当前会话、未提交,/rewind 能把代码和对话一起撤回到某个 checkpoint。
有没有一个文件数上限,超过重构就不可靠了? 没有硬上限,但 Anthropic 的建议把单次会话大约 20-40 个文件当作可靠区间;超过之后准确率下降,应该把活儿拆成多个阶段,每阶段之间 commit 一次。
相关阅读
- Claude Code 改错文件
- Claude Code Plan 模式偏离计划
- Claude Code 覆盖既有改动
- Claude Code 新手入门
- Claude Code 工作流
- Claude Code 项目配置
标签: #排查 #Claude Code #排查 #Scope 蔓延