Why Agents Botch Big Refactors (and How to Scope Them So They Don't)
I asked Claude Code to rename a core domain type across my app — Account to Workspace, plus three fields hanging off it. Maybe sixty files. It started fast: the first dozen were clean, imports updated, call sites renamed, types threaded through. I let it run.
About thirty files in, it drifted. It started re-renaming Account in files it had already touched twenty minutes earlier, because by then those edits had fallen out of its context. In two files it half-applied the change: the type was Workspace, but a method still returned the old shape. The typecheck I ran at the end had ninety errors, some in code the agent had "finished."
None of this was the model being dumb. It was me handing one agent a job bigger than it could hold in its head, with no way to check itself along the way — the agentic equivalent of telling a junior dev "go rename this everywhere" and walking away for an hour.
The short version: agents fail at big refactors for a structural reason, not an intelligence one — the change is larger than the context they can reliably attend to, and nothing forces them to verify before they drift. You fix it by scoping: plan first, cut the work into independently-verifiable steps, run a deterministic check between each, and keep a written checklist the agent re-reads instead of remembering. Where the change is mechanical, skip the agent and use a codemod.
Why the wheels come off
Three things compound on a large refactor, and they all trace back to context.
The window fills and attention rots. Anthropic's own best-practices doc is blunt about it: "Claude's context window fills up fast, and performance degrades as it fills... Claude may start 'forgetting' earlier instructions or making more mistakes" (Claude Code best practices). This isn't unique to Claude. The "lost in the middle" effect — where models attend worst to information sitting in the middle of a long context — is well documented, and research shows degradation can start well before the window is even full. As a refactor runs, every file read and every command output piles into the same finite window, pushing your original instructions and the invariants you stated into the low-attention middle.
The effective window is smaller than the sticker. Cursor users hit this constantly: a model with a 200K advertised window often has only ~40–60K usable tokens once the harness's own overhead, system prompt, and tool outputs are accounted for, and "by the third or fourth step, the agent is operating with degraded awareness of what it did in step one" (Morph, Augment Code on Cursor multi-file refactors). A refactor that spans 50 files at 200K tokens of source simply does not fit, so files 31–50 get edited by an agent that's already forgotten files 1–30.
Cross-file contracts are invisible to a file-at-a-time editor. A refactor's whole risk lives in the seams: the caller that assumed the old return shape, the fixture built around the old field name, the serializer that still writes the old key. The agent edits file A correctly and file B correctly, but the contract between them is a thing no single file states. That's where half-applied changes and broken invariants live.
For a clean rename like mine, the honest first question is whether an agent should touch this at all.
Use a codemod when the change is mechanical
If the transformation is structural and rule-shaped — rename a symbol, change a call signature, swap an import — a deterministic AST codemod beats an agent on every axis that matters. Tools like jscodeshift operate on the abstract syntax tree, so they apply the exact same transform to every matching node, in parallel, across a codebase "in seconds," with no chance of two files drifting apart (Toptal, Martin Fowler on codemods). An agent, by contrast, "will also have a chance of hallucination, giving slightly different results each time, and it is slow and costly" on a large codebase (Carlos Cuesta).
The move that actually uses the agent's strength: have it write the codemod, not perform the edit. You describe the transform in English, it generates the jscodeshift transform, you test it on three files, then run it deterministically across all sixty. You get the agent's fluency at expressing the rule and the AST tool's guarantee that the rule applies identically everywhere.
# Agent writes transforms/account-to-workspace.js, you run it:
npx jscodeshift -t transforms/account-to-workspace.js src/ --dry # preview
npx jscodeshift -t transforms/account-to-workspace.js src/ # apply
git diff --stat # 60 files, one deterministic transform
My rename was 80% mechanical. The right call was a codemod for the renames and an agent only for the ~12 files needing judgment — the serializer, the migration, the places where Account and Workspace had to coexist during a transition. Reach for the agent where the change requires reading intent, not where it requires applying a rule.
Plan first, in a mode that can't edit
When the change genuinely needs an agent, do not let it start editing on turn one. The single biggest lever is separating exploration and planning from execution, so the agent commits to an approach before it touches disk.
In Claude Code that's plan mode (Shift+Tab, or --permission-mode plan): the agent reads files, traces imports, and produces a plan while making zero edits, and you approve it first (common workflows). Aider has the equivalent split — /architect to plan the restructuring, then /code to execute it (refactoring a large codebase with aider). The point isn't ceremony. Letting an agent "jump straight to coding can produce code that solves the wrong problem," and a plan you read in two minutes catches that before it costs you an hour of edits (best practices).
The doc draws a clean line on when to bother: "If you could describe the diff in one sentence, skip the plan." A multi-file refactor never fits in one sentence. Plan it.
A useful detail for big jobs: don't accept the first plan that decomposes into "edit all 60 files." Push it to refactor in dependency order — start with the modules nothing else depends on and work outward — and to split the work into phases small enough to verify. In practice, agents stay reliable across roughly 20–40 files in one pass; past that, the consensus is to split into phases and run them as separate sessions with a commit between each (Claude Code multi-file refactoring walkthrough).
Cut it into independently-verifiable steps with a gate between each
This is the part that actually saves you. A refactor the agent does in one undifferentiated sweep has no internal checkpoints, so the first error silently corrupts everything after it. A refactor cut into steps — each one ending in a check that passes or fails — can't propagate a mistake past the gate that catches it.
Anthropic frames the whole discipline around this: "Claude stops when the work looks done. Without a check it can run, 'looks done' is the only signal available, and you become the verification loop." Give it a check that returns pass/fail — a test suite, a build exit code, a typecheck — "and the loop closes on its own" (best practices). The check converts "the agent thinks it's done" into "the typechecker agrees it's done."
Here's the staged plan I should have used for the Account → Workspace rename, in dependency order, with a gate after every step:
PLAN: Account -> Workspace rename. Commit after each step that passes the gate.
[ ] 1. Rename the type + fields in src/domain/ (no dependents).
GATE: tsc --noEmit clean in src/domain/. Commit.
[ ] 2. Update the data layer (repositories, serializers) that import the type.
GATE: tsc --noEmit clean; unit tests for src/data/ pass. Commit.
[ ] 3. Update service/API layer call sites.
GATE: tsc --noEmit clean across repo; integration tests pass. Commit.
[ ] 4. Update test fixtures + factories to the new field names.
GATE: full test suite green. Commit.
[ ] 5. Update UI components + any remaining string references.
GATE: tsc --noEmit clean; build succeeds; e2e smoke passes. Commit.
Each step is small enough to fit in context, ends in a deterministic gate, and is committed before the next begins. If step 3 throws ninety errors, you see it at step 3 — with steps 1 and 2 already safe in git — not at the end across all sixty files. The commit matters as much as the gate: a botched step is one git reset away, not an archaeology project.
Tell the agent to run the gate itself and show the output, not assert success. "Have Claude show evidence rather than asserting success: the test output, the command it ran and what it returned" (best practices). To hard-enforce it, a Stop hook can run the check as a script and refuse to let the turn end until it passes.
Keep the checklist in a file the agent can't lose
The plan above only helps if the agent still has it in step 5. It won't, if it lives only in the conversation — by step 5 the early steps have aged out of the window, which is exactly the failure we started with. So the checklist goes in a file: PLAN.md or REFACTOR.md in the repo, with the agent instructed to read it, do the next unchecked step, run the gate, and tick the box.
Anthropic recommends precisely this for long, exhaustive jobs: for "code migrations, fixing numerous lint errors, or running complex build scripts," have Claude "use a Markdown file (or even a GitHub issue!) as a checklist and working scratchpad" (best practices). A file in the repo is external memory. The agent re-reads it each turn, so the plan stays at full attention instead of decaying in the middle of a bloated context. The ticked boxes are the durable record of what's actually done — not what the agent vaguely recalls doing.
Pair this with context hygiene. Between steps — or between phases run as separate sessions — /clear so the next step starts clean, with only the checklist and the current step's files loaded. Delegate any "go read how X works" exploration to a subagent so the reading happens in a separate window and only the summary comes back. (Both of these have their own posts here — I won't re-litigate them.)
A verification pass that didn't write the code
One last gate, after the steps are done: review the whole diff with a fresh agent that never saw the implementation. A reviewer "running in a fresh subagent context sees only the diff and the criteria you give it, not the reasoning that produced the change, so it evaluates the result on its own terms" (best practices). Point it at the plan:
Use a subagent to review the full diff against REFACTOR.md. Check that every
step's change is fully applied — no file left with the old type or old field
names, no method still returning the old shape. Report only gaps that break
correctness or a stated requirement, not style.
That last sentence matters. A reviewer told to find gaps will manufacture them — "extra abstraction layers, defensive code, and tests for cases that can't happen" — so scope it to correctness and contract, or you'll trade a clean refactor for a week of gold-plating (best practices).
The thing I keep relearning: the agent isn't the bottleneck on a big refactor — the scope is. A 60-file rename handed over whole will drift, half-apply, and lie about being done, no matter how good the model is. The same rename cut into five dependency-ordered steps, each gated by a typecheck, each committed, tracked in a file the agent re-reads — that one finishes, because at no point did I ask it to hold more than it could. Plan it, slice it, gate it, write it down. And when the change is just a rule, write the codemod and let the AST do the part agents are worst at.
