One type error became an architecture change
一个类型错误演变成了架构改动
To fix a single type error, the agent changes an interface, then updates every implementer, and a one-file request becomes a multi-file redesign.
Applies to: Claude Code · Cursor · Codex · Copilot agent
Symptom
The diff's center of gravity is an interface or type file you didn't mention, with mechanical edits rippling across many modules.
Why it happens
Changing the type at the root is the most "complete" fix from the compiler's point of view. Night Forge lists this cascading refactor as one of three recurring wrong-file patterns.
Example
From the Night Forge write-up.
textAsked: fix a type error in OrderService.ts
Agent: changed the Repository<T> interface, then updated all 9 implementers
Copy-paste fixes
Pick the files for the agents you use. Snippets are in English so they work in any repo.
Claude Code: add to CLAUDE.md
CLAUDE.md## Fix locally first
- Fix type and compile errors at the call site when possible (narrow, convert, or adapt locally).
- Do not change a shared interface, base class or exported type to fix a single caller.
If that seems necessary, stop and propose it with the list of affected implementers.
- Keep the diff within the files you listed before starting.
Codex, Copilot coding agent and others: add to AGENTS.md
AGENTS.md## Fix locally first
- Fix type and compile errors at the call site when possible (narrow, convert, or adapt locally).
- Do not change a shared interface, base class or exported type to fix a single caller.
If that seems necessary, stop and propose it with the list of affected implementers.
- Keep the diff within the files you listed before starting.
Cursor: project rule
.cursor/rules/cascading-refactor.mdc---
description: "AgentFails: One type error became an architecture change"
alwaysApply: true
---
## Fix locally first
- Fix type and compile errors at the call site when possible (narrow, convert, or adapt locally).
- Do not change a shared interface, base class or exported type to fix a single caller.
If that seems necessary, stop and propose it with the list of affected implementers.
- Keep the diff within the files you listed before starting.
Diff budget check (run before commit or in CI)
Adapted from the diff-budget post on dev.to (yureki_lab). Reads the real git diff, so it also catches edits made through the shell. Usage: 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")
Claude Code PreToolUse hook: block edits outside the declared scope
Adapted from the diff-budget design on dev.to (yureki_lab). Put allowed globs, one per line, in .agent-scope at the repo root. Exit code 2 blocks the tool call and feeds stderr back to the agent. It only covers file-edit tools; shell writes still need the diff check.
.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)