Using AI agents inside a messy legacy codebase

April 20, 20264 min readBy Harman Kamboj
AIEngineering

Running an AI agent in a legacy codebase is a different sport than using one on a clean greenfield project. The demos all look the same: a tidy little app, a clear request, a perfect diff. Real legacy code is nothing like that. It is ten years of decisions, half of them undocumented, conventions that contradict each other between modules, and a test suite that is either missing or lying to you. Drop a model into that and it will confidently produce code that looks plausible and fits nothing, because it cannot see the unwritten rules that actually govern the place.

The context problem is the whole game

A model only knows what you put in front of it. In a clean codebase the patterns are obvious, so even a thin slice of context is enough for the model to match the surrounding style. In a legacy system the patterns are buried, inconsistent, and sometimes wrong, and the model has no way to know which of three competing approaches is the one your team actually blessed last year. So it picks one, often the most generic one from its training, and now you have a fourth approach to maintain.

My fix is to do the archaeology myself and feed the model the conclusions. Before I ask it to touch anything, I find the existing pattern it should follow and point directly at it: this file, this function, do it like this. That single act of grounding does more for output quality than any amount of clever prompting, because it replaces the model's guesswork with your hard-won knowledge of the codebase.

Scope it down hard

The instinct to hand a model a big vague task gets punished badly in legacy code. 'Refactor the billing module' invites it to wander through code it does not understand and make changes whose blast radius it cannot estimate. I keep tasks small enough that I can fully review the diff and predict what it touches. A narrow change in a system I understand beats a sweeping change in a system the model is improvising about.

Some scoping rules that keep me out of trouble:

  • One concern per change, so the diff stays reviewable and revertible.
  • Point at a concrete existing example to copy rather than describing the convention in words.
  • Keep the model away from code with no tests until you have added some.
  • Treat any change that crosses module boundaries as a human job, not an agent job.

Build a safety net before you let it loose

The scariest thing about agents in old code is that legacy systems are exactly where you cannot see what you broke. There are no tests on the gnarly path, the behavior is load-bearing in ways nobody remembers, and a small change three layers down surfaces as a bug in a feature that looks unrelated. Before I let a model modify anything important, I get characterization tests around the current behavior, even if the current behavior is technically wrong. The point is not correctness. The point is a tripwire that fires when something changes, so I find out from a red test instead of from a customer.

Where it genuinely earns its keep

For all the warnings, agents are great at some of the worst parts of legacy work. Understanding is the big one. I point a model at a tangled function and ask it to explain what the thing actually does, trace the data flow, and flag the edge cases. It is faster at reading unfamiliar code than I am, and a wrong explanation is cheap because I am going to verify it anyway. That alone saves hours of squinting at code written by someone who left the company in 2019.

The other strong fit is mechanical, repetitive change. Migrating a deprecated API call across a hundred files, updating a pattern that repeats everywhere, writing the tedious tests the original authors skipped. These are jobs where the work is boring but the correctness is checkable, which is the sweet spot. The model does the typing, you do the judging, and the boring parts of legacy maintenance stop eating your week.

You are still the one who owns it

The throughline is that an agent in a legacy codebase is an amplifier of your judgment, not a replacement for it. If you understand the system and scope the work and hold a real safety net, it makes you faster at genuinely tedious tasks. If you hope it will understand the mess so you do not have to, it will cheerfully add to the mess and hand you a clean-looking diff that quietly breaks something nobody will notice for a month. Old code does not forgive optimism, from a model or from you.

Building something where this matters?

I am open to senior full-stack, Web3, or AI engineering roles, fully remote and any timezone. If the hard part of your product is fighting you, that is the work I like.

Get in touch →