Overreach

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.

text
Asked: 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.

Rules

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.
Rules

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 rule

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.
CI / script

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")
Hook

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)

Sources and credit

Related patterns

翻车 of the week

One real agent failure and its fix, every week. Free, short, no spam.