Claude Code Refactor Scope Becomes Too Broad

Asked for a one-file rename, got a 12-file diff? Enforce the boundary with a permissions deny rule, scope every prompt with an editable-file list, and park adjacent issues in FOLLOWUPS.md.

You asked Claude Code to rename a function in billing.ts. The diff comes back with 12 files changed, 200 lines added: a “consistency pass” across 4 unrelated modules, formatting you didn’t want, and a “minor improvement” to import order in 8 files. The task was 5 minutes; review is now 30.

Fastest fix: don’t rely on the prompt alone. Add a permissions.deny rule so Claude Code physically cannot edit files outside the target, then give every prompt an explicit editable-file list and send adjacent findings to FOLLOWUPS.md instead of fixing them. Without a boundary, Claude follows code paths it considers “related”: imports lead to callers, callers lead to other modules, modules have their own inconsistencies, and your one-file task becomes a refactor.

One thing to internalize before the steps: a permissions rule is enforced by the Claude Code harness; a CLAUDE.md instruction is not. The official docs are explicit that “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.” So prompt-layer fences (Steps 2-3) reduce the urge to expand; the permissions deny rule (Step 1) is the only thing that actually stops a write.

Which bucket are you in?

Symptom in the diffMost likely causeJump to
Rename applied in files you never mentionedPrompt named no file boundaryStep 1, Cause 1
One core change plus many small call-site editsAgent followed import/call chainsCause 2
Import-order / formatting / naming churn unrelated to the taskUnprompted consistency passCause 3
Broad changes that “match” a CLAUDE.md ruleA standing “clean up while you’re at it” authorizationStep 3, Cause 4
The whole diff is genuinely requiredReal multi-file refactor bundled into one PRStep 5, Cause 5
Many test files changed alongside the renameCascading “test fix”Step 6, Cause 6

Common causes

Ordered by hit rate, highest first.

1. Prompt didn’t name the file boundary

“Rename getUser to findUser” doesn’t say where. Claude renamed it everywhere, including legacy code paths you’ve been migrating away from, where the rename actually broke the migration.

How to spot it: re-read your prompt. If it doesn’t list specific files, the boundary was implicit and Claude expanded.

2. Agent followed import/call chains into adjacent files

Claude renamed getUser in service.ts, then updated all 8 call sites across the codebase. Each call site is a legitimate touch, but the cumulative scope is 9x what you intended.

How to spot it: diff includes a “core” file change plus many small call-site updates. Each is necessary; together they are scope expansion.

3. Agent applied a consistency pass unprompted

Claude noticed half the files use import type and half don’t. While renaming, it “harmonized” them across 6 files. None of that was asked for.

How to spot it: diff has changes unrelated to the rename (import order, formatting, naming). These are pure scope creep.

4. CLAUDE.md or prompt authorized “clean up while you’re at it”

# CLAUDE.md
- Improve code quality where you see opportunities
- Fix obvious issues during related work

Innocent-looking rules; standing authorizations for scope expansion.

How to spot it: grep -i "improve\|clean up\|while you're\|opportunit" CLAUDE.md. Each match is a creep authorization.

5. Refactor by nature touches many files

Some refactors genuinely span the codebase (rename a public API, migrate a state library). The scope is real, but bundling it into one PR is the problem. It should have been split. Anthropic’s own guidance puts the reliable ceiling around 20-40 files in a single session; past that, split into phases with a commit between each.

How to spot it: the diff is legitimately required. The bug is “one mega-PR” instead of “PR chain.”

6. Agent’s “test fix” cascaded

Tests failed after the core change. Claude “fixed” them by updating expectations, mocks, and fixtures across many test files. Some are legit (matching the new signature); some are masking failures.

How to spot it: many test-file changes accompany the rename. Audit which ones are signature updates versus which are silently weakening assertions.

Shortest path to fix

Ordered by ROI. Step 1 is the only hard enforcement; Steps 2-3 shape behavior at the prompt layer.

Step 1: Enforce the boundary with a permissions deny rule

This is the part the original mega-diff was missing. Add an Edit deny rule to .claude/settings.local.json so Claude Code blocks any write outside the files you intend to touch, regardless of what the prompt or CLAUDE.md says. Edit rules apply to every built-in file-editing tool and to file-writing Bash commands Claude Code recognizes (sed, etc.).

{
  "permissions": {
    "deny": [
      "Edit(/src/**)"
    ],
    "allow": [
      "Edit(/src/services/user.ts)",
      "Edit(/src/services/user.test.ts)"
    ]
  }
}

Rules evaluate deny first, then ask, then allow, so the broad Edit(/src/**) deny is overridden only by the two explicit allow paths. Paths use gitignore semantics: a leading / is the project root, so Edit(/src/**) means <project>/src/**. (A bare /Users/... is NOT absolute; absolute paths need a double slash, Edit(//tmp/scratch.txt).) Verify the rule list any time with /permissions, which shows every rule and the settings file it came from.

If you’d rather block edits everywhere by default and open only what you list, run the session in dontAsk mode (auto-denies tools unless pre-approved via permissions.allow):

claude --permission-mode dontAsk

For a permanent guard on files that should never be touched casually (lockfiles, generated code, CI config), keep a deny rule in the checked-in .claude/settings.json:

{
  "permissions": {
    "deny": [
      "Edit(/**/*.lock)",
      "Edit(/dist/**)",
      "Edit(/.github/workflows/**)"
    ]
  }
}

Step 2: Hard editable-file list in the prompt

The deny rule stops writes; the prompt list keeps Claude from trying (and from filling the transcript with blocked-edit retries).

Editable files (DO NOT touch anything else):
- src/services/user.ts
- src/services/user.test.ts

For any other file:
- If the change requires touching it, STOP and ask before editing.
- Do NOT make consistency / formatting / cleanup changes anywhere.

The “stop and ask” line lets Claude flag a genuine cascade without unilaterally expanding.

Step 3: Redirect adjacent issues to FOLLOWUPS.md

If you spot issues in files outside the editable list:
- Do NOT fix them.
- Add a line to `FOLLOWUPS.md` describing the issue and its file:line.
- These become future tasks, not this PR.

Now Claude can be “helpful” without expanding scope. Every helpful spot becomes a documented future task. Pair this with a CLAUDE.md audit:

grep -in "improve\|clean up\|while you're there\|opportunistically" \
  CLAUDE.md src/**/CLAUDE.md prompts/*.md

Replace each loose authorization with a tighter rule:

- Do NOT make changes outside the immediate task scope.
- Adjacent issues go in FOLLOWUPS.md, not into the current PR.

Step 4: Plan first, so scope is visible before any edit

For anything that might cascade, start in plan mode: Claude reads files and runs read-only commands but cannot edit source until you approve a plan. Press Shift+Tab to cycle the permission mode (it steps through default, accept-edits, then plan; the footer shows the current mode), or launch with claude --permission-mode plan. You then see the file list it intends to change and can shrink it before a single write happens. Approve the plan to execute, or keep refining.

One known wrinkle as of June 2026: cycling out of plan mode with Shift+Tab without approving a plan can inject a “work without stopping for clarifying questions” reminder (anthropics/claude-code#59203). If you want to abandon a plan, clear it explicitly rather than tab-cycling past it.

Step 5: For a received over-broad diff, revert the out-of-scope files

Don’t merge as-is, don’t reject entirely. Revert just the out-of-scope changes.

Whole-file revert when an entire file shouldn’t have been touched:

git diff --stat HEAD                       # see every changed file
git checkout HEAD -- <out-of-scope-files>  # restore them to HEAD

Hunk-level revert when a single file has both wanted and unwanted changes:

git checkout -p src/services/user.ts
# answer "y" to discard each out-of-scope hunk, "n" to keep the real change

Then confirm the core change still passes:

pnpm test

If the over-broad edit happened in the current uncommitted session, /rewind (Claude Code’s checkpoint-based undo, available in v2) rolls code and conversation back to an earlier point in one command. Prefer it over git reset --hard, which is destructive and discards uncommitted work you may want.

Step 6: For genuine multi-file refactors, split into a PR chain

If the rename truly requires updating 8 callers, ship it staged:

PR 1: Add `findUser` alongside `getUser` (no caller changes; both exist).
PR 2: Migrate callers in `src/services/billing/` (3 callers).
PR 3: Migrate callers in `src/api/` (5 callers).
PR 4: Remove `getUser`.

Each PR is reviewable in 5 minutes. The total diff is the same; reviewability is 10x.

Step 7: Verify the tests weren’t weakened

For each test file in the diff:

git diff -- src/services/user.test.ts

# Flag removed expect/assert lines, added .skip, broadened type checks
git diff -- src/services/user.test.ts | grep -E '^-.*expect|^-.*assert|^\+.*skip'

If assertions got removed or tests skipped, the “test fix” was a regression hidden as a fix. Restore the original tests and re-prompt to fix the production code instead.

How to confirm it’s fixed

  1. git diff --stat HEAD lists only the files you intended (plus their tests).
  2. /permissions shows the Edit deny rule in effect, and re-running the task produces a blocked-edit message for any out-of-scope file instead of a silent write.
  3. The grep over test diffs returns nothing (no removed assertions, no new .skip).
  4. FOLLOWUPS.md has grown with the adjacent issues Claude noticed but did not touch.

Prevention

  • Keep a checked-in Edit deny rule for files that should never be casually edited (lockfiles, generated code, CI config); enforcement beats good intentions.
  • Give every code prompt an explicit editable-file list with a “stop and ask” rule for anything outside it.
  • Run cascade-prone tasks in plan mode so the intended file set is visible before any write.
  • Maintain a FOLLOWUPS.md so Claude can surface adjacent issues without acting on them.
  • Audit CLAUDE.md for “improve while you’re there” authorizations and remove them.
  • Split genuine multi-file refactors into a PR chain instead of one mega-PR; expect ~20-40 files as the reliable per-session ceiling.
  • Spot-check test diffs: assertion removal or skip additions are creep disguised as fixes.
  • Review against the original scope, not the final diff. The diff may look reasonable on its own.

FAQ

Why does Claude Code ignore “only edit billing.ts” in my prompt? Because a prompt instruction is not a boundary. The model treats it as guidance and will cross it when it judges a related file “needs” the change. Only a permissions.deny rule (Step 1) is enforced by the harness. Put the rule in .claude/settings.local.json and the prompt instruction becomes a hint on top of a hard wall.

deny vs. plan mode: which should I use to limit scope? Use both, for different jobs. Plan mode prevents all edits until you approve a plan, which is ideal for exploring scope first. A deny rule allows edits but blocks specific paths, which is ideal once you know exactly which files are in bounds. Plan mode for discovery; deny rules for execution.

How do I let Claude edit one file but block the rest of src/? Combine a broad deny with narrow allows, remembering deny is evaluated first: "deny": ["Edit(/src/**)"] plus "allow": ["Edit(/src/services/user.ts)"]. Paths are relative to the project root because of the single leading slash.

Claude already made a 12-file mess. What’s the safest way to keep only the part I wanted? Don’t git reset --hard. Use git checkout HEAD -- <file> to revert whole out-of-scope files and git checkout -p <file> to discard individual hunks inside a mixed file. If the changes are still uncommitted in the current session, /rewind undoes both code and conversation to a checkpoint.

Is there a file-count limit where refactors get unreliable? There’s no hard cap, but Anthropic’s guidance treats roughly 20-40 files per session as the reliable range; beyond that, accuracy drops and you should split the work into phases with a commit between each.

Tags: #Troubleshooting #Claude Code #Debug #Scope creep