Refactor legacy code with AI: a safe, test-first workflow for agents

Refactor legacy code with AI: a safe, test-first workflow for agents

To refactor legacy code with AI safely, first pin down what the code does today with characterization tests, then let a coding agent make small, behavior-preserving changes that the tests check after every step. This guide covers mapping an unfamiliar codebase with an agent, writing those tests, running incremental refactors, and the guardrails that keep risk low.

In shortTL;DR
  • Never let an agent refactor code that has no tests; have it write characterization tests that capture current behavior first, bugs included.
  • Use the agent to map the codebase before changing it: entry points, data flow, hidden side effects, and who calls what.
  • Refactor in small commits that each keep the tests green, and keep refactoring separate from behavior changes.
  • Put the rules in CLAUDE.md or AGENTS.md: no public API changes, no new dependencies, run tests after every edit.
  • Review diffs for silent behavior changes: error handling, ordering, rounding, time zones and logging are where agents slip.

Can you refactor legacy code with AI safely?

Yes, if tests come before changes. Refactoring legacy code with AI is safe when an agent works against a test suite that captures the current behavior and runs it after every small edit. It is risky when an agent "cleans up" untested code in one large change, because nothing tells you which of a thousand changed lines quietly altered behavior.

Coding agents such as Claude Code and Codex are good at the mechanical parts of refactoring: reading unfamiliar code, tracing call chains, writing tests from observed behavior, renaming and extracting across dozens of files, and running the suite repeatedly. What they cannot see is the undocumented contract: the report a finance team builds from your log lines, the downstream job that depends on a specific sort order, the rounding rule a customer was promised in 2014.

The workflow in this guide has five phases:

  1. Map the code with the agent before touching it.
  2. Pin current behavior with characterization tests.
  3. Write the rules down where the agent reads them.
  4. Refactor in small, verified steps.
  5. Review for silent behavior changes and merge often.

How do you map an unfamiliar codebase with an AI agent?

Ask the agent to explain before you ask it to change. A mapping session gives you a written picture of entry points, data flow and risk areas, and it gives the agent the context it needs to refactor well later.

Start in read-only mode. In Claude Code, plan mode lets Claude read files and propose plans without editing anything: start with claude --permission-mode plan, or press Shift+Tab mid-session until the status bar shows plan mode is on.

Then ask questions that surface risk, not only structure:

prompt
I need to refactor the billing module (src/billing/). Before any changes:1. List every entry point into this module (HTTP routes, jobs, CLI commands, imports from other modules).2. Trace one invoice from creation to PDF output, naming each function.3. List side effects: database writes, file writes, network calls, emails, log lines that look parsed by something else.4. List global state, singletons, environment variables and time or locale dependencies.5. Point out code that looks dead, and how confident you are that it is.Write the result to docs/billing-map.md. Do not edit any code.

For a large codebase, exploration fills the context window quickly. The Claude Code docs suggest delegating research to a subagent, which reads files in its own context and returns only a summary: "use a subagent to investigate how invoices are numbered." Keep the resulting map in the repository. It is useful to the next person and to the next agent session.

Tip

Ask the agent how sure it is about each finding, and verify the "dead code" list with search and runtime logs before deleting anything. Code that looks unused is often called through reflection, configuration or a cron job outside the repo.

How do you write characterization tests with AI?

Characterization tests record what the code does today, so any change in behavior fails a test. The idea comes from Michael Feathers' Working Effectively with Legacy Code: before improving code, capture its current outputs for a wide range of inputs, wrong answers included.

This is one of the best jobs to give an agent. It is tedious for people, and the agent can run the code to observe real outputs instead of guessing.

prompt
Write characterization tests for calculate_invoice() in src/billing/invoice.py.Goal: capture CURRENT behavior, not correct behavior. If the output looks like a bug,keep the assertion as it is and add a comment "characterizes current behavior, possible bug".Cover: zero and negative quantities, discounts above 100%, missing tax region,currency rounding, very large amounts, leap-day dates.Generate expected values by running the current code, never by reasoning about what it should return.Put the tests in tests/characterization/ and run them. All must pass on the current code.

For functions with complex outputs, a golden master (snapshot) test is often simpler. Record inputs and outputs to files once, then compare on every run:

tests/characterization/test_invoice_golden.py
import jsonimport pathlibimport pytestfrom billing.invoice import calculate_invoiceGOLDEN = pathlib.Path(__file__).parent / "golden"CASES = json.loads((GOLDEN / "inputs.json").read_text())@pytest.mark.parametrize("case", CASES, ids=lambda c: c["name"])def test_invoice_matches_golden_output(case):    expected = json.loads((GOLDEN / f"{case['name']}.json").read_text())    assert calculate_invoice(**case["args"]) == expected

JavaScript projects can use Jest's toMatchSnapshot() for the same effect. Whatever the tool, check three things before trusting the suite:

  • It passes on the untouched code. If a characterization test fails before any refactor, it is wrong.
  • It catches breakage. Ask the agent to make a deliberate small change (flip a rounding mode, swap a sort order) and confirm a test fails. Anthropic's migration guide makes the same point: validate your judge against deliberately broken code.
  • It is not tautological. Tests that mock the function under test or assert only that "no exception was raised" protect nothing. The guide on writing unit tests with AI covers how to spot them.

What rules should you give the agent before refactoring?

Write explicit refactoring rules in the project's instructions file, so every session follows them without you repeating them. For Claude Code that is CLAUDE.md; Codex and several other agents read AGENTS.md.

CLAUDE.md
## Refactoring rules for this repo- Behavior must not change. Characterization tests in tests/characterization/ must stay green.- Run `make test` after every edit that touches src/. Stop and report if a test fails twice.- Do not change public function signatures, HTTP routes, database schema or log formats.- Do not add dependencies. Do not upgrade dependencies as part of a refactor.- One refactoring per commit (extract, rename, move, inline). No drive-by fixes.- If you find a bug, write it down in docs/found-bugs.md and leave it in place.- Mark anything you are unsure about with a `# REFACTOR-CHECK:` comment and list it in your summary.

The last rule borrows a pattern from Anthropic's migration write-up, where uncertain translations were flagged with // TODO(port) comments and handled in a later pass. Markers like these turn "the agent was unsure" from invisible into searchable. More on structuring these files in CLAUDE.md best practices.

How do you run an incremental refactor with an agent?

Ask for one small, named refactoring at a time, verified by the tests, committed on its own. Small steps keep diffs reviewable and make any failure easy to trace back.

A loop that works:

  1. Plan. In plan mode, ask for a sequence of small refactoring steps toward a goal ("split the 2,000-line BillingService into invoice, tax and PDF modules"). Edit the plan until each step is one named refactoring.
  2. Execute one step. "Do step 1 only: extract tax calculation into billing/tax.py. Run the tests. Commit with a message naming the refactoring."
  3. Check. Read the diff. It should be mostly moved lines and updated imports.
  4. Repeat. Move to the next step only when tests are green and the diff looks like what the plan described.
Refactor typeRisk with an agentWhat to watch
Rename, move, extract functionLowMissed dynamic references (strings, reflection, config)
Split a large class or moduleMediumCircular imports, changed initialization order
Replace a library or APIMedium to highDifferent defaults, error types, time zone handling
Change concurrency or async modelHighRace conditions tests rarely catch
Data model or schema changeHighMigrations, existing data, other services reading the table

For the higher-risk rows, split work even further and add targeted tests first. Some changes are better done by a deterministic codemod (such as jscodeshift for JavaScript or OpenRewrite for Java) that the agent writes and runs, since a codemod applies the same transformation everywhere without per-file judgment calls.

For whole-system replacements, use the strangler fig pattern: build the new component alongside the old one, route traffic piece by piece, and retire the legacy code when nothing calls it. Agents are good at building the seams (interfaces, adapters, routing flags) that make this possible. Framework and language upgrades follow the same logic at larger scale, covered in AI code migration.

What should you look for when reviewing AI refactoring diffs?

Look for behavior changes the tests do not cover. Agents tend to "improve" things while refactoring, and the most common slips are small and plausible.

Review checklist for agent refactors:

  • Error handling: a broad except narrowed, an exception type changed, a swallowed error now raised (or the reverse).
  • Ordering: dictionary or set iteration replacing a list, a sort removed, a query without ORDER BY.
  • Numbers: float math replacing decimal, rounding at a different step, integer division changes.
  • Time: naive and aware datetimes mixed, local time replacing UTC, date parsing with a different format.
  • Logging and output: log messages reworded that a monitoring rule or another script parses.
  • Defaults: a parameter default changed, a None check turned into a truthiness check (now 0 and "" behave differently).
  • Scope creep: files touched that the step did not name.

Ask the agent to review its own diff against this list in a fresh session, then do your own pass. A second agent with a clean context catches things the first one rationalized; Anthropic's migration guide uses separate adversarial reviewers for this reason. The guide on AI code review covers setting this up on every pull request.

Warning

Do not let an agent "fix" failing characterization tests by updating expected values. If a test fails, the refactor changed behavior. Revert the step or decide, as a person, that the change is intended.

How do you keep a long refactor moving without losing control?

Break the work into independent tasks, run them on separate branches, and merge small pull requests often. Long-lived refactor branches drift from main and become their own risk.

Practical setup for multi-day refactors:

  • One branch per step or module, merged as soon as it passes. Git worktrees let several agent sessions work on separate branches of the same repo without colliding; Claude Code supports this with claude --worktree <name>. See git worktrees for parallel agents.
  • A written queue. Keep the plan as a checklist file in the repo. Each agent session picks the next unchecked item, does it, checks it off. If a session dies, the next one resumes from the file.
  • Feature flags for anything that changes a runtime path, so you can switch back without a deploy.
  • Resume, don't re-explain. claude --continue resumes the most recent session in the current directory, so context from the mapping phase is not lost between sittings.

Long refactors are also where agents run for hours. If you would rather not keep a laptop awake for that, Orkio runs Claude Code on an always-on cloud box: you queue a refactor step from your phone or the web, close the app, and review the pull request when it is ready.

A checklist to start refactoring legacy code with AI this week

Start with one module that hurts, and prove the workflow on it before scaling up.

  1. Pick one module that changes often and breaks often.
  2. Run a read-only mapping session and save the map to docs/.
  3. Have the agent write characterization tests; confirm they pass on current code and fail on a deliberate break.
  4. Add refactoring rules to CLAUDE.md or AGENTS.md.
  5. Plan small named steps; execute one per commit with tests after each.
  6. Review every diff with the checklist above, then get a second-agent review.
  7. Merge small pull requests daily and keep a list of bugs you found but did not fix.
  8. Only after the module is under test and cleaner, start fixing the bugs you logged, each as its own change with its own test.

Frequently asked questions

Can AI rewrite a legacy codebase from scratch?

It can produce a rewrite quickly, but a big-bang rewrite carries the same risk it always did: the old code encodes years of edge cases nobody documented. Incremental refactoring or a strangler fig migration, with tests comparing old and new behavior, is far safer. Anthropic's own migration guidance starts by building a test suite that judges both versions.

What are characterization tests?

Characterization tests record what code currently does, rather than what it should do. You feed real inputs, capture the outputs, and assert that future versions produce the same outputs. The term comes from Michael Feathers' book Working Effectively with Legacy Code.

Which AI tool is best for refactoring legacy code?

Agents that can read the whole repository and run your test suite, such as Claude Code or Codex, work better than chat windows for refactoring because they verify their own changes. Editor assistants help for small, local refactors. The tool matters less than having tests the agent can run.

How do I stop an AI agent from changing behavior during a refactor?

Give it a test suite it must keep green, tell it explicitly that behavior must not change, and forbid changes to public interfaces. Ask it to list any place where it was unsure whether behavior changed. Then review the diff yourself, focusing on error paths and edge cases.

Is it safe to paste proprietary legacy code into an AI tool?

Check your plan's data terms first. Business and enterprise plans from the major vendors do not train on your data by default, while consumer plan settings vary. Many companies require an approved tool and a business plan before any internal code is shared.

SOURCES

  1. Claude Code docs: Common workflows
  2. Anthropic: How we run large-scale code migrations with Claude Code
  3. Martin Fowler: Strangler Fig Application
  • #refactoring
  • #legacy code
  • #claude code
  • #testing
  • #ai coding
Orkio team

The team building Orkio. We write about putting AI to work: coding agents that run overnight, always-on boxes and pull requests ready by morning.

Keep reading

All posts

07:06 · YOU MERGED #128

Go to sleep. Wake up to pull requests.

Start for $19 a month
© 2026 Orkio · Hosted in Europe TürkçeStatus