Short answers to what readers ask most about this topic.
01Why are AI agents good at legacy code migration?
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.
02What should I do before migrating legacy code with an agent?
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.
03How do I run a legacy migration at scale with agents?
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.
04Where do AI agents struggle on legacy code?
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.
05Do I still need human review for agent-led migrations?
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.
Framework upgrades and language ports are repetitive, well-specified, and verifiable, exactly the shape AI agents handle well. How to run one safely with a test net.
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.
Why migration suits agents
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:
It is bounded, because the target framework or language version defines exactly what done looks like.
It is repetitive, so the same transformation applies across dozens or hundreds of files, which is tiring for a human and trivial for an agent.
It is verifiable, because the existing system is a reference oracle: the migrated code should behave the same, and you can test that.
Build the safety net first
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
done
Give 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 migration workflow
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.
Inventory the surface area by listing the files, modules, and APIs that the migration touches.
Characterize behavior with tests that pin the current outputs, especially for code that has no tests today.
Migrate in small slices, one module or one pattern at a time, so each change is reviewable.
Verify each slice against the characterization tests before moving on, and stop the batch on the first regression.
Review the diff with a human or a fresh agent, since a passing test still misses behavior nobody thought to capture.
Where agents struggle on legacy
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.
Scaling with headless and review
Once one migration works end to end, the pattern scales, as long as the review gate scales with it.
Fan out across modules with headless runs, one agent per unit, logging every result for later inspection.
Keep the characterization suite as the gate every migrated unit must pass before it counts as done.
Batch related changes into reviewable pull requests rather than one enormous diff no human can read.
Track progress from the logs, and re-run only the units that failed instead of the whole set.
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.