只让修一处,它改了一堆无关文件
Asked for one fix, it changed unrelated files
你只要求改一处;diff 里却顺手“清理”了没人提过的其他函数、文件和配置。
适用于: Claude Code · Cursor · Codex · Copilot agent
症状
一个五行的修复变成了改了十二个文件的 diff。真正的改动被埋在里面;审查时间翻了十倍;回滚时还得连带撤掉无关改动。几轮下来,你已经不知道它到底改了什么。
为什么会这样
在任务内部看,顺手整理邻居代码像是在“把活干好”;而“只改必要的部分”这种话只是建议性的上下文,不是强制。Kaplira、Night Forge 和 dev.to 的 diff budget 文章得出了同样的分层:规则表达意图;权限和钩子拦截工具;范围与 diff 的比对兜底。agent-failure-modes 称之为 AFM-2 范围破坏。
示例
摘自 dev.to 的 diff budget 文章。
textTask: "fix the off-by-one in the pagination helper"
Agent also: renamed two variables "for clarity", converted a callback to async/await,
"aligned" a similar pattern in a neighboring module, edited a linter config.
复制即用的修复
按你用的智能体挑选文件。规则片段用英文写,放进任何仓库都能用。
Claude Code:加入 CLAUDE.md
CLAUDE.md## Scope rules
- Only modify files the request needs. Reading other files is fine; editing them is not.
- Before the first edit, list the files you expect to change. If you then need another file,
stop and explain why before editing it.
- No opportunistic refactors, renames, reformatting, comment rewrites or config changes.
- Put anything you noticed but didn't fix into FOLLOWUPS.md (file, issue, suggested task).
- Finish with `git diff --stat` and justify every changed file against the request.
Codex、Copilot 编码智能体等:加入 AGENTS.md
AGENTS.md## Scope rules
- Only modify files the request needs. Reading other files is fine; editing them is not.
- Before the first edit, list the files you expect to change. If you then need another file,
stop and explain why before editing it.
- No opportunistic refactors, renames, reformatting, comment rewrites or config changes.
- Put anything you noticed but didn't fix into FOLLOWUPS.md (file, issue, suggested task).
- Finish with `git diff --stat` and justify every changed file against the request.
Cursor:项目规则
.cursor/rules/unrelated-files-changed.mdc---
description: "AgentFails: Asked for one fix, it changed unrelated files"
alwaysApply: true
---
## Scope rules
- Only modify files the request needs. Reading other files is fine; editing them is not.
- Before the first edit, list the files you expect to change. If you then need another file,
stop and explain why before editing it.
- No opportunistic refactors, renames, reformatting, comment rewrites or config changes.
- Put anything you noticed but didn't fix into FOLLOWUPS.md (file, issue, suggested task).
- Finish with `git diff --stat` and justify every changed file against the request.
Claude Code PreToolUse 钩子:拦截声明范围之外的编辑
改编自 dev.to(yureki_lab)的 diff budget 设计。在仓库根目录的 .agent-scope 中每行写一个允许的 glob。退出码 2 会阻止该工具调用,并把 stderr 反馈给智能体。它只覆盖文件编辑工具,shell 写入仍需靠 diff 检查兜底。
.claude/hooks/scope_guard.py#!/usr/bin/env python3
"""PreToolUse hook: block Edit/Write/MultiEdit outside the globs in .agent-scope."""
import fnmatch, json, os, sys
call = json.load(sys.stdin)
if call.get("tool_name") not in ("Edit", "Write", "MultiEdit"):
sys.exit(0)
root = os.environ.get("CLAUDE_PROJECT_DIR") or call.get("cwd") or os.getcwd()
scope_file = os.path.join(root, ".agent-scope")
if not os.path.exists(scope_file):
sys.exit(0) # no scope declared for this task -> allow
allowed = [l.strip() for l in open(scope_file) if l.strip() and not l.startswith("#")]
target = os.path.relpath(call["tool_input"]["file_path"], root)
if any(fnmatch.fnmatch(target, pat) for pat in allowed):
sys.exit(0)
print(
f"BLOCKED: {target} is outside this task's scope ({', '.join(allowed)}).\n"
"If this edit is required, stop and ask the user to widen .agent-scope, with a one-line reason.\n"
"If it is just an improvement you noticed, add it to FOLLOWUPS.md and keep going.",
file=sys.stderr,
)
sys.exit(2)
在 .claude/settings.json 中注册范围守卫
.claude/settings.json{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{ "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/scope_guard.py" }
]
}
]
}
}
Diff 预算检查(提交前或在 CI 中运行)
改编自 dev.to(yureki_lab)的 diff budget 文章。读取真实的 git diff,所以通过 shell 做的修改也逃不掉。用法:python3 scripts/diff_budget.py origin/main 6 150
scripts/diff_budget.py#!/usr/bin/env python3
"""Fail if the diff vs BASE touches files outside .agent-scope or exceeds the budget."""
import fnmatch, os, subprocess, sys
base = sys.argv[1] if len(sys.argv) > 1 else "HEAD"
max_files = int(sys.argv[2]) if len(sys.argv) > 2 else 6
max_lines = int(sys.argv[3]) if len(sys.argv) > 3 else 150
allowed = []
if os.path.exists(".agent-scope"):
allowed = [l.strip() for l in open(".agent-scope") if l.strip() and not l.startswith("#")]
out = subprocess.run(["git", "diff", "--numstat", base], capture_output=True, text=True, check=True).stdout
untracked = subprocess.run(["git", "ls-files", "--others", "--exclude-standard"],
capture_output=True, text=True, check=True).stdout.split()
files, lines, outside = 0, 0, []
rows = [r.split("\t") for r in out.strip().splitlines()] + [["0", "0", p] for p in untracked]
for added, deleted, path in rows:
files += 1
if added != "-":
lines += int(added) + int(deleted)
if allowed and not any(fnmatch.fnmatch(path, p) for p in allowed):
outside.append(path)
problems = []
if outside:
problems.append("out-of-scope files: " + ", ".join(outside))
if files > max_files:
problems.append(f"{files} files changed (budget {max_files})")
if lines > max_lines:
problems.append(f"{lines} lines changed (budget {max_lines})")
if problems:
print("OVER BUDGET: " + "; ".join(problems))
sys.exit(1)
print(f"within budget: {files} files, {lines} lines")
团队共享的 /verify 技能(Claude Code)
把它提交进仓库,让整个团队(以及每个智能体会话)跑同一套检查。把命令换成你项目里真实的命令。
.claude/skills/verify/SKILL.md---
name: verify
description: Verify the current change before calling it done. Use before every commit, PR, or "done" message.
---
# Verify
Run every step and paste the real output. Never summarize a step you did not run.
1. **Scope**: run `git diff --stat` and `git status --short`. For every changed file, say which part
of the request needs it. Any file you cannot justify gets reverted, or listed under
"Unrequested changes" for the user to decide.
2. **Build and types**: `npm run build` (or the project's build command).
3. **Tests**: `npm test`. Report pass/fail counts. If a test was edited, skipped or deleted,
say so explicitly with the reason.
4. **Behavior**: exercise the changed behavior once for real (a curl, a CLI run, a screenshot)
and show what happened.
5. **Report** in this format:
- Requested: <one line>
- Changed files: <list, each with its reason>
- Checks run: <command -> result>
- Not done / unsure: <list, or "none">
来源与致谢
- Kaplira: When an AI agent changes unrelated files
- Night Forge: Stop your AI agent editing the wrong files
- dev.to (yureki_lab): How I stopped my AI coding agent's scope creep with a diff budget
- claudecodeguides.com: Stop Claude Code from modifying unrelated files
- agent-failure-modes: AFM-2 Scope Vandalism
- @JC_bitcoin on X (Chinese): after a few rounds of Codex edits I no longer know what it changed
- @Sc_Meerkat on X: Opus deleted a test unrelated to the functionality it was working on
- @BVeiseh on X: the agent changed something consequential it wasn't asked to change