Intermediate · 20 min read · updated 2026-07-14

How to Refactor Legacy Code with AI Using Claude Code

A field-tested way to refactor legacy code with AI: map the codebase first, wrap it in tests, then let Claude Code cut it into small, reversible steps.

Claude CodeRefactoringLegacy CodeAI EngineeringTesting
Who this is for: engineers sitting on a codebase nobody fully understands anymore - a 2015 PHP monolith, a jQuery-and-inline-CSS frontend, a Rails app three major versions behind - who want to use an AI coding agent to move it forward without breaking production on day one.

Most teams that try to refactor legacy code with AI make the same mistake: they open Claude Code, paste in a messy file, and say "clean this up." That works for a 200-line script. It falls apart on a 40,000-line codebase with undocumented business rules buried in nested conditionals. The fix isn't a smarter prompt - it's a different workflow, one built around mapping before touching anything, tests as the actual safety net, and refactors small enough that a bad one costs you ten minutes, not a week.

I've run this process on three real legacy migrations this year (a WordPress-to-headless move, a decade-old Django billing module, and a client's abandoned Vue 2 app with zero tests). The steps below are what actually held up.

Why "refactor legacy code with AI" needs a different approach than greenfield work

On a new project, an AI agent can generate code and you verify it against a spec that exists in your head. On legacy code, the spec IS the code - and often the code contradicts itself because six developers touched it over eight years with no shared conventions. Claude Code (or any agent) has no idea which quirks are load-bearing business logic and which are dead weight from a 2019 feature that got cut.

This means the first 30-40% of the job is not refactoring at all. It's building a map that both you and the model can trust. Skip this and you'll get confident, well-formatted code that silently drops an edge case a customer depends on.

Warning: never let an agent "clean up" a legacy file in one shot without first generating characterization tests for its current behavior. A refactor that changes output silently is worse than no refactor.

Step 1: Map the codebase before you touch anything

Point Claude Code at the repo and ask it to build a written map, not code. Concretely:

  • Ask for a dependency graph of the top 20 most-imported modules ("which files does everything else route through?").
  • Ask it to list every place a specific business term appears (e.g. invoice_status, is_trial) and summarize the different code paths that reference it.
  • Ask it to flag dead code candidates: exported functions with zero call sites, feature flags that are always the same value in every environment.

Save this output as a markdown file in the repo (docs/legacy-map.md) and treat it as a living document. This is also the point where a project CLAUDE.md earns its keep - once Claude Code has learned "this repo's utils/ folder is actually three unrelated utils files that got merged in 2021, do not assume shared conventions," write that down so every future session starts with it instead of re-discovering it.

A concrete mapping prompt that works

Read src/billing/*.py. Do not suggest changes yet.
Produce a table: function name | called from (file:line) | what business
rule it enforces (infer from code + comments) | confidence (high/medium/low).
Flag anything with confidence "low" as needing a human to confirm intent.

The "confidence" column matters more than it looks. It tells you exactly where the model is guessing, which is where you review by hand before anything gets touched.

Step 2: Get a safety net before the first refactor commit

You cannot safely refactor legacy code with AI (or without it) unless you can prove behavior didn't change. If the codebase already has good test coverage, skip to Step 3. Most legacy codebases don't - so build characterization tests first: tests that pin down what the code currently does, not what it should do.

  • Ask Claude Code to generate characterization tests for the messiest functions first - the ones with the most call sites and the least documentation.
  • Record real production inputs and outputs where possible (sanitized), and assert the refactored function produces identical output for those exact inputs.
  • Don't aim for 100% coverage. Aim for coverage on the functions you're about to touch, expanding as you go.
// characterization test - pins current behavior, not "correct" behavior
test('calculateProration matches legacy output for known account states', () => {
  const cases = require('./fixtures/proration-real-samples.json');
  for (const { input, expectedLegacyOutput } of cases) {
    expect(calculateProration(input)).toEqual(expectedLegacyOutput);
  }
});

This is also where an eval-driven development mindset pays off: treat each characterization test suite as an eval set. If you can't articulate "these ten cases must still pass," you're not ready to refactor that module.

Step 3: Slice the refactor into steps small enough to revert in a minute

The single biggest lever for safety is step size, not model quality. A refactor plan should read like a checklist where each item is independently committable and independently revertible.

Good slice sizes:

  • Extract one function out of a 500-line file, with tests passing, as its own commit.
  • Rename a misleading variable/function across the codebase in one commit, no logic changes mixed in.
  • Replace one direct database call with a repository method, one call site at a time.

Bad slice sizes (the ones that cause 2am rollbacks):

  • "Refactor the entire orders module to use the new pattern" in one PR.
  • Renaming AND restructuring AND adding types in the same commit.
  • Any step you can't describe in one sentence.

Use Claude Code's plan mode to force this discipline: have the model draft the full slice-by-slice plan, review it before any file changes, and reject any step that bundles more than one kind of change. This is also a good place to run each independent module refactor in its own git worktree, so a stuck refactor in the payments module doesn't block progress on the auth module.

Comparison: refactor strategies at a glance

StrategyRiskSpeedWhen to use
Big-bang rewriteVery highFast to start, slow to shipNever, for anything with paying customers
Strangler fig (new code wraps old, old is retired gradually)LowSlow but steadyLarge modules you must keep running during the migration
Slice-by-slice in placeLow-mediumSteadyMost legacy refactors, this guide's default
AI does the whole file in one promptHighFast, feels productiveSmall scripts under ~150 lines with existing tests only

Step 4: Use subagents to separate "understand" from "change"

A pattern that reduces mistakes noticeably: run mapping/analysis and code-writing as separate agent roles rather than one long conversation. With Claude Code subagents, you can set up a read-only "codebase analyst" subagent that only ever reads and reports, and a separate "refactor executor" that only acts on an approved plan. This stops the common failure mode where an agent that's been staring at messy code for an hour starts making unrequested "improvements" mid-task.

Pair this with permission scoping: give the analyst subagent read-only tool access, and give the executor write access limited to the specific files in the current slice. It's a small config change that prevents an entire class of "it also touched three unrelated files" incidents.

Step 5: Review AI-generated diffs like you'd review a junior's PR, not like you'd review your own code

The failure mode isn't usually broken syntax - the model gets that right almost every time now. It's plausible-looking logic changes that quietly alter behavior: an >= that became >, a null check that got "simplified" away, a default value that changed from 0 to null.

  • Diff review, not full-file review. If the model touched 12 lines in a 400-line file, review those 12 lines against the characterization tests, not the whole file.
  • Run the test suite after every single slice, not at the end of the day.
  • For anything financial, security-related, or touching auth, require a second human reviewer regardless of test results - tests catch what you thought to test, not what you forgot to.

A dedicated pass with AI code review tooling on top of your own review catches a different class of issue than a human skim does, and it's cheap enough to run on every slice.

Cost and time reality check

Refactoring legacy code with Claude Code is not free and it's not instant - budget for it honestly:

  • Mapping phase on a mid-size repo (30-60k lines): typically 3-6 hours of agent + human time, spread over a day or two so you can sanity-check the output.
  • Characterization tests for one gnarly module: 2-4 hours, most of it you gathering real sample inputs, not writing test code.
  • Each refactor slice, once the plan exists: 10-30 minutes including review, for slices sized as described above.
  • Token spend adds up fast if you re-explain context every session - see managing Claude Code token costs for how caching and a good CLAUDE.md cut this by half or more on long-running refactor projects.
Tip: run the whole slice-by-slice plan through your CI pipeline before merging to main, and use the production checklist for vibe-coded and AI-refactored code as your final gate. It exists precisely for this handoff moment.

FAQ

Can Claude Code refactor an entire legacy codebase in one session?

No, and you shouldn't want it to. Context windows and review bandwidth both cap how much can safely change at once. Treat a full legacy refactor as a multi-week program made of small, independently testable slices, not a single prompt or a single session.

What if the legacy codebase has no tests at all?

Start with characterization tests on the functions you're about to touch, using real (sanitized) production inputs and outputs as fixtures. You don't need full coverage before starting - you need coverage on today's slice before you commit today's change.

How do I stop the AI from "improving" code I didn't ask it to change?

Use plan mode to lock in an approved list of files and changes before execution starts, and scope write permissions to just those files for that session. Bundling unrequested cleanup into a refactor PR is one of the most common ways trust in AI-assisted refactors breaks down on a team.

Is it safe to let AI refactor code that handles payments or auth?

It's safe if the process includes characterization tests against real historical data, small reviewable slices, and a mandatory human second reviewer for that category of change - the same standard you'd hold a human contractor to. Skipping any one of those three is where the risk actually comes from, not the AI itself.

How much of the legacy codebase should I map before starting?

Map the top 20-30% of files by import count and by business-term density first - that's usually where 80% of the risk and undocumented logic lives. Peripheral, low-traffic modules can be mapped just-in-time, right before you touch them.

Ready to tackle your legacy codebase?

If you're staring down a legacy system and want a second opinion on where the real risk is before you start cutting, grab a free 30-minute call - book through the contact form or message us directly on wa.me/972585802298.

Sources