Legacy code. Everyone who's touched it knows the feeling.
You inherit a service nobody's touched in three years. Inside is an 800-line orderService.js, functions nested in functions, a comment reading "TODO: optimize later," timestamp 2022. You want to add a new payment channel, change two lines, tests go red, CI fails, the frontend dev pings you in the group chat. You revert, exhale, and decide never to touch this thing again.
Now you have Claude Code and Cursor, and you figure you're saved. You open Claude Code and type "refactor orderService.js for me." It churns out 600 lines of new code, functions split nicely, names tasteful, looks legit. You git diff: 412 lines changed across 7 files. One seemingly harmless variable rename also rewrote a reference in another file-somewhere that shouldn't have been touched. You don't dare merge. You revert again.
Tools leveled up, the legacy code didn't. The problem isn't that the AI is dumb. It's that you handed an engineering-discipline job to a partner with no discipline. This SOP turns that discipline into 5 steps, so AI helps you refactor legacy code without burying you and your team.
1. Step One: Build the Safety Net Before Anything Else
Michael Feathers' line in Working Effectively with Legacy Code applies more now than a decade ago: "Before you refactor, have tests." In the AI era this gets amplified-AI changes fast; without a test net catching it, the faster it changes, the faster you die.
Passing the existing tests is the floor. The first thing you do with an inherited service isn't open the AI. It's run npm test / pytest / go test ./.... Green earns you the next step. Red means fix the red first-refactoring on a broken foundation is letting AI build on quicksand.
No tests? Write characterization tests. Legacy code likely has no unit tests, and backfilling all of them is unrealistic. First add "snapshot tests" or "golden cases" for the few most critical functions-no need for pretty test names, just lock in "input X produces output Y." Claude Code, which reads project context, is well suited for this. Give it a precise prompt:
# Task: Add characterization tests for calcFinalPrice in orderService.js
# DO NOT modify business code.
# Background: This function hasn't been touched in 3 years. Nobody dares
# refactor it because there are no tests.
# Steps:
1. Read the implementation of calcFinalPrice in web/src/services/orderService.js
2. Find every call site in the project (grep calcFinalPrice)
3. Collect real input samples from call sites-cover normal/edge/error paths, at least 5
4. Write snapshot tests in web/tests/orderService.spec.js asserting current output
(even if the output is wrong, don't fix it-just lock the behavior)
5. Run npm test -- orderService; must be all green
# Constraints:
- Do NOT modify any code in orderService.js
- Match the style of existing project tests (read any file under web/tests/)The core of this prompt is "lock the current behavior"-even buggy behavior, lock it first. The sole goal of refactoring is "external behavior equivalence." You have to know what the original output was before you can verify nothing broke. Claude Code reads files, greps call sites, writes the test file, runs the tests, reads the error, fixes the test-all without you switching windows. This is exactly what its project-level context capability is for.
No CI? Build a manual safety net locally: npm test && npm run lint && npm run typecheck in sequence, run after every refactoring step. Put it in package.json under scripts.safety-one command, and you know immediately when something's off.
2. Step Two: Define Refactoring Boundaries-One Type of Change at a Time
The most common reason AI refactoring goes wrong isn't that the AI is dumb. It's that you let it do too much at once. "Refactor this file" packs in: split functions, extract constants, rename, move files, swap data structures. Each is safe alone; stacked together, the diff becomes an indecipherable mess nobody dares review.
One type of change per pass-this is discipline, not a suggestion. Cut refactoring into non-overlapping steps:
- Rename: only change names, not behavior
- Extract function: only pull a block into a function, logic unchanged
- Replace magic numbers: only lift literals to constants
- Move files: only relocate, content untouched
- Change data structures: standalone step, must have test backing
Each step gets its own commit, with a message stating "what this commit does only." If a step goes red, git reset --hard HEAD~1 and have the AI redo it. Small steps aren't a slogan in the AI era-they're the only engineering means that lets you locate a rollback point in 5 minutes.
Claude Code's plan mode fits this rhythm naturally. Add "produce a plan first, don't execute" to the prompt, and it outputs a refactoring plan for you to review-then you decide which step to let it do and which to hold. Far safer than letting it charge ahead-during planning, the diff is zero, a zero-cost review moment.
Lock down the boundaries: telling the AI what not to touch matters more than telling it what to do. Refactoring boundary prompt template:
# Refactoring task: split calcFinalPrice in orderService.js
# Boundaries (must follow):
- Files allowed: web/src/services/orderService.js only
- Action allowed: extract pure calculation logic into helpers/price.ts,
keep naming consistent
- Files NOT to touch: paymentService.js, couponService.js
(these call calcFinalPrice; don't change the signature)
- Behavior NOT to touch: outputs for all existing inputs must match
the snapshot tests
- Do NOT introduce new dependencies
# Acceptance:
- npm test all green
- git diff touches only orderService.js and the new helpers/price.tsThis prompt treats the AI like an intern who can read code but needs explicit instructions. Under vague instructions it improvises; under explicit ones it's steadier than anyone.
3. Step Three: Give the AI Precise Context
The second biggest reason AI refactoring fails: too little context. The AI doesn't know project constraints, which files are off-limits, or the project's naming style, so it falls back to generic best practices-and generic best practices can be flat wrong in your project.
Claude Code's project-level context relies on two things: CLAUDE.md and @ file references.
CLAUDE.md sits at the project root and is auto-loaded by Claude Code at the start of every session. Before refactoring, write the project's red lines in it: which test framework, naming style, which directories are legacy and untouchable, which are the new stack. Write it once, save yourself on every subsequent refactor. Claude Code's docs call CLAUDE.md a "memory file"-that's exactly what it's for.
@ references feed it specific files: @web/src/services/orderService.js @web/tests/orderService.spec.js. "Read this file" isn't enough-tell it "this is what's being refactored, this is the test," and it won't conflate test data with business code.
Cursor's multi-file editing takes a different path: @ multiple files in Composer, or use Agent mode and let it open files itself. Cursor is strong at multi-file collaborative editing and the diff review panel, weaker on long-term project memory (its .cursorrules file works like CLAUDE.md but has a thinner ecosystem). Each tool has its strengths-here's a real-world comparison:
| Dimension | Claude Code | Cursor |
|---|---|---|
| Project memory | CLAUDE.md, auto-loaded | .cursorrules, manual |
| Multi-file edits | via Read/Edit tools, steady but slow | native Composer, fast |
| Diff review | git diff fallback | built-in review panel, good UX |
| Refactoring plan | plan mode, zero-diff proposal | Agent mode, edits as it thinks |
| Best for | long sessions, deep context, cross-dir big changes | mid/small scope, visual diff, fast iteration |
Choosing isn't either/or: use Cursor for bounded mid-sized refactors; use Claude Code's plan mode for cross-directory big refactors where you need the full picture first. Both backed by git diff. Trust neither blindly.
4. Step Four: Step-by-Step Execution + Manual Diff Review
At this point the safety net is up, boundaries are drawn, context is fed. Time to move. But "move" doesn't mean "delegate." Every step the AI changes, you review.
Single-step refactoring prompt template (copy, swap paths, ready to use):
# Single-step refactor: extract discount calc from calcFinalPrice
# into a standalone function
# Context:
- File to change: @web/src/services/orderService.js
- Test file: @web/tests/orderService.spec.js
- Project rules: @CLAUDE.md
# This step ONLY:
1. Extract the discount logic at lines 120-145 of calcFinalPrice
into a new function applyDiscount(cart, coupon)
2. applyDiscount goes in the same file; signature
(cart: Cart, coupon: Coupon) => number
3. Don't change calcFinalPrice's external signature or return value
4. Don't touch other files
# After execution, you MUST:
- Run npm test -- orderService and paste the test output
- Paste the git diffThe key is the last two lines: have the AI run tests and paste the diff itself. Claude Code can run shell commands and read git output, so having it paste results after doing the work is far more reliable than a "done" from you.
Diff review prompt (AI changed, you review):
# Review this diff for potential issues
# Check specifically:
1. Whether changes exceed the boundaries I set (e.g., touched paymentService.js)
2. Whether function signatures stay unchanged externally
3. Whether new side effects were introduced (new imports, globals, console.log)
4. Whether there are "drive-by improvements" (renamed vars, tweaked unrelated code)
5. Whether types are compatible (TypeScript projects-must check)
# Output format: list suspicious points + line numbers; if none, reply "no issues"
# This is a review request, not a re-edit. Only report problems; do NOT fix them.The last line matters. AI defaults to "fix on sight," so by the time you review it has produced another version and you don't know which to review. Have it report only, hands off. The decision is yours.
Run tests every step, don't batch. If the AI changes three steps and you run tests once, and step 2 broke something, you can't tell whether it was step 2 or step 3-you have to roll back two. One step, one run; red means git stash or git reset immediately. Localization cost is near zero.
5. Step Five: Regression Verification + Cleanup
The last step of the 5-step method is the most skipped, because by the time you've done the first four you're tired and want to merge. That's exactly when things blow up.
Full regression: run the entire test suite, not just the part you refactored. Legacy code's dependency graph usually exceeds what you assume-orderService changes one signature, and invoiceService downstream calls it in a corner case the unit tests don't cover. Only the integration tests surface that. npm test all green, CI all green, then proceed.
Manually verify critical paths: what automation can't cover, like the "place order-pay-issue ticket" chain, click through by hand. No matter how thorough the tests, they don't replace a human eye running the real flow once.
Clean up the AI's leftovers. After a refactor, AI leaves a pile of things to clean:
- Debug
console.log/print/fmt.Println - Commented-out old code (it didn't dare delete, so it commented)
- Unused imports
- Temp variable names (
newVar2,temp_result) - Self-appointed JSDoc / docstrings whose content may be wrong
Write "refactor" in the commit message, not "fix" or "feat." That way git log shows at a glance which commits are behavior changes and which are structural. When debugging an issue, skipping refactor commits saves a lot of time.
Finally, leave a "rollback plan": keep the refactoring PR standalone, don't mix it with feature changes in one PR. If something goes wrong in production after merge, git revert <refactor commit> rolls it back without touching other changes. This discipline matters more than any AI tool.
6. Five Real Pitfalls
These 5 pitfalls are all from real projects, not hypothetical.
Pitfall 1: Letting the AI change too much at once, a big-bang diff nobody can review
Once told Cursor's Agent mode to "clean up this controller." It churned through 8 files and 600 lines in one go. The PR looked green, nobody dared approve, and we reverted and started over. Fix: cap a single task at "one file, one type of change, diff under 30 lines." Anything more, split it.
Pitfall 2: Skipping the test net, finding the breakage a week later
Skipped step one and had Claude Code refactor an untested utility function. It looked fine, merged it. A week later a user reports "the price is wrong." Tracing back, the AI had changed Math.round to Math.floor because "floor is more reasonable." No malice, but without a test catching it, this "reasonable but not behavior-equivalent" change is a bug. Fix: even 3 characterization test cases beat going bare by 100x.
Pitfall 3: Trusting the AI's "tests passed"-it never ran them
Claude Code does run tests for you. But when you ask it to "confirm tests passed," it sometimes replies "tests passed" by inferring they should pass, without actually running them-especially when the context window is tight, late in a long session, it slacks off. Fix: make it paste the raw output of the test command. Seeing PASS / FAIL strings counts. No raw output, no deal.
Pitfall 4: Cross-file cascade edits out of control
Cursor's multi-file editing is a double-edged sword. You have it change one function in file A, and it follows the import chain rewriting B, C, and D too, citing "consistency." Good intentions, but when you review, the A changes were expected and B/C/D are complete surprises. Fix: write explicitly in the prompt "files allowed: [list]; do not change other files even if related."
Pitfall 5: Refactoring mixed with business logic changes, behavior not equivalent
This is the most insidious pitfall. While refactoring, the AI spots "obviously buggy" logic and fixes it on the spot. Refactoring and bug-fixing are two things; mixing them in one PR means you can't tell which line is structural and which is behavioral during review, and you can't roll back selectively. Fix: write into the prompt "behavior equivalence principle: outputs for all existing inputs must match pre-refactor; log found bugs as separate issues, don't fix them in the refactor." Michael Feathers' iron law still applies in the AI era.
7. Don't Let AI Carry Your Discipline
Tools are only as strong as your discipline is loose. Claude Code's plan mode and Cursor's diff panel are amplifiers for people who already have discipline. Give it clear boundaries, precise context, a run-every-step rhythm, and it can safely turn three-year-old untouchable legacy code into something maintainable. Cut corners with a one-liner "clean it up" and you get a 600-line PR nobody dares merge, ending in another revert.
The 5-step method looks verbose, but every step saves time: the test net saves debugging time afterward, boundaries save review time, stepwise execution saves rollback-location time. Sharpening the axe doesn't waste chopping time-the old saying still holds in the AI era.
Next time you see that 800-line orderService.js, don't sigh. Set up the test net, draw the boundaries, give the AI precise context, go one step at a time, review each step, then regression-verify. You'll find you're braver about touching it than you thought.
References
- Claude Code official docs (CLAUDE.md, plan mode, tool invocation): https://docs.anthropic.com/en/docs/claude-code/overview
- Cursor official docs (Agent mode, Composer, .cursorrules): https://docs.cursor.com
- Michael Feathers, Working Effectively with Legacy Code-the canonical source for characterization tests and the behavior-equivalence principle