AI Agents for Legacy Code Migration: A Guide

Migration is bounded, repetitive, and verifiable, which is the opposite of the open-ended tasks where agents struggle. The target framework or language version defines what done means, the same transformation applies across many files, and the existing system is a reference oracle you can test the result against. That shape plays directly to an agent's strengths.
Build a safety net first. Characterization tests assert what the code does today rather than what it should do, turning the old system into an executable specification the agent must preserve. Without them you are trusting the agent's understanding of code nobody else remembers, and silent behavior changes will slip through unnoticed.
Use headless mode to script one agent per module, so a failure in one unit stays isolated instead of derailing the batch. Give each agent a single module and a passing-test target, log every result, and re-run only the units that failed. Batch related changes into reviewable pull requests rather than one enormous diff.
They struggle with the tribal knowledge that never made it into the code: a workaround for a vendor bug, an ordering a downstream job silently depends on. An agent reads syntax fluently but confidently modernizes undocumented behavior away. A quirk that looks like a bug is sometimes load-bearing, which is exactly what characterization tests are there to catch.
Yes. A passing characterization test still misses behavior nobody thought to capture, so keep a human on the merge button. Simon Willison's work on async code-research agents suggests letting agents attempt migrations in throwaway or parallel branches, treating the output as a draft, and having a person decide what actually merges.

Key Takeaway
Legacy migrations, framework upgrades, and language ports are bounded, repetitive, and verifiable, which makes them a strong fit for AI agents run in headless batches. The one non-negotiable is a safety net: characterization tests that pin current behavior before a single line changes, so you notice when an agent modernizes a rule out of existence.
Legacy migration is the work nobody wants and every codebase eventually needs: move off the deprecated framework, port the old service to a supported language, bump the dependency that three hundred files still import. It is tedious, mechanical, and risky in the same breath, which is exactly why it is one of the best jobs to hand an AI agent.
The catch is that legacy code is legacy precisely because nobody remembers why it does what it does, and an agent will cheerfully rewrite behavior it does not understand. This guide covers why migration suits agents, how to build the safety net first, and where the work still goes wrong.
Most agent tasks are open-ended, and open-ended is where agents struggle. Migration is the opposite: the destination is defined, the transformation repeats across many files, and success is checkable against the old behavior. That shape plays to an agent's strengths.
Three properties make migration a standout use case:
Before you migrate anything, capture what the code does today. Characterization tests, which assert current behavior rather than intended behavior, turn the old system into an executable specification the agent must preserve. Without them you are trusting the agent's understanding of code nobody else understands either.
Once the net is in place, migration parallelizes cleanly. Headless mode lets you script one agent per module, so a failure in one unit stays isolated instead of derailing the batch:
# One headless agent per module, isolated failures
for mod in $(cat modules.txt); do
claude -p "Port $mod from Enzyme to React Testing Library. \
Keep behavior identical. Run npm test -- $mod until it passes." \
--output-format json >> migration.log
doneGive each agent one module and one passing-test target, not the whole repo. Small, isolated tasks keep failures contained, make progress easy to track in the log, and let you retry a single unit without rerunning everything that already succeeded.
A repeatable migration follows the same arc whether you are upgrading a framework or porting a language. Move in slices, and verify each slice before the next.
The failures cluster around the things legacy code hides. An agent reads syntax fluently but cannot see the tribal knowledge that never made it into the code: the workaround for a vendor bug, the ordering that a downstream job silently depends on.
Simon Willison's work on async code-research agents suggests a useful posture: let agents attempt migrations in throwaway or parallel branches, treat the output as a draft, and keep a human deciding what actually merges. The agent explores; you approve.
Agents confidently modernize undocumented behavior away. A quirk that looks like a bug is sometimes load-bearing, and an agent will clean it up without hesitation. Characterization tests are what catch this: without them the regression ships silently and surfaces in production weeks later.
Once one migration works end to end, the pattern scales, as long as the review gate scales with it.
AI agents are genuinely good at legacy migration, but only inside a harness that assumes they will get the subtle cases wrong. Build the characterization net, migrate in verifiable slices, fan out with headless runs, and keep a human on the merge button. Do that and the migration nobody wanted becomes a weekend of supervised batches instead of a quarter of dread.