TL;DR: A legacy code AI method looks like archaeology, not a tour: form a specific hypothesis about why strange code exists, make AI name what evidence would prove that hypothesis wrong, then go find it before you touch anything. A model can't see your codebase's history, so you have to supply it.
How Do You Use AI to Work Through Code You Don't Trust?
You don't ask a model to explain legacy code. You ask it to propose a hypothesis, name what would falsify that hypothesis, and then you go check. That sequence is built around one fact that's easy to forget mid-conversation: a fluent explanation is not evidence, and a model that sounds certain about why your billing function has that odd early return is pattern-matching against similar code it saw in training, not reading your incident history.
Legacy code earns that caution in a way ordinary unfamiliar code doesn't. Nobody wrote a comment explaining the strange branch. Nobody who remembers why it's there still works here. There's a real chance the tests, if there are any, were written after the fact and only cover what someone thought to check. Every "why" question you ask about this kind of code is really a request for a guess, dressed as an answer, unless you give the model something to reason from besides the code itself.
Why Isn't This the Same Problem as Learning a New Codebase?
Because "new to you" and "undocumented and orphaned" are different problems with different failure modes. A codebase you've simply never seen before is usually a reliable narrator: it has docs, an active team, and tests that reflect what it's supposed to do. You can trust what it tells you about itself, even if you need help parsing it. A five-phase method for orienting, mapping, and tracing through that kind of unfamiliar code covers exactly that case, and it's worth doing first if your problem is genuinely "I'm new here," not "nobody here remembers this."
Legacy code, in the sense this post means it, doesn't cooperate that way. It may have no docs, no active maintainer, and tests that were bolted on later, if at all. The code is the only witness left, and witnesses that can't be cross-examined need to be treated with more suspicion, not less. That's the whole reason archaeology, not orientation, is the right frame here: you're not being shown around a system by someone who understands it. You're reconstructing intent from artifacts, the same way an archaeologist reconstructs a culture from pottery shards instead of asking the people who made them.
What Can an AI Model Actually See When You Paste It Legacy Code?
Only the text you gave it, and only in the sense of pattern-matching against similar text, not lived experience of why anyone wrote it. Two limits matter here, and they compound.
First, scope: a model can't see your whole codebase unless you paste it, and even generous context windows don't change the underlying limit, because a codebase isn't just files, it's years of decisions, incidents, and conversations that never made it into any file at all. Second, and more important: even a model that somehow read every line you've ever written still wasn't in the room when someone decided a check was necessary. It has no access to the production incident, the customer escalation, or the Slack thread that produced that check. It can describe what the code does. It cannot know why, unless you hand it the evidence, because "why" isn't encoded in syntax.
This isn't a reason to avoid AI here. It's a reason to change what you ask for. Instead of "explain this function," ask for a hypothesis and a falsification test. The difference sounds small. It changes everything about what you do with the answer.
What Is Chesterton's Fence, and Why Does It Apply to Every Odd Line of Legacy Code?
Chesterton's Fence is the principle that you don't remove something until you understand why it was put there, and it's a near-perfect fit for legacy code because legacy code is full of fences nobody explained before they left.
The idea comes from G. K. Chesterton's 1929 book The Thing, in the chapter "The Drift from Domesticity". Chesterton frames it as a fence across a road: "There exists in such a case a certain institution or law; let us say, for the sake of simplicity, a fence or gate erected across a road." A reformer who doesn't see the point wants it gone. Chesterton's answer to that reformer: "If you don't see the use of it, I certainly won't let you clear it away. Go away and think." Only once you can explain the fence's purpose does he grant you permission to tear it down.
Apply that to a codebase and the fence is every odd conditional, every seemingly redundant check, every function that does more than its name suggests. The instinct on first read is the same as the reformer's: this looks unnecessary, so let's simplify it. The discipline is the same too: find out what it's guarding against before you touch it, not after.
Why Is the Weird Branch Usually the Bug Fix?
Because the ugliest-looking line in an otherwise clean function is very often not sloppiness, it's a patch. Something broke for a specific input, customer, or environment the general case didn't cover, someone added a narrow fix under time pressure, and the fix stayed exactly as narrow as the incident that caused it. Next to tidy code, that fix looks like clutter. It reads as an accident. It usually isn't one.
This is the single most expensive mistake in legacy-code cleanup: deleting the ugly special case because it looks like someone's mistake, when it's actually the record of someone else's mistake already having happened and already having been fixed. You don't clean up the function. You reopen the incident, quietly, and you often don't find out until the same input shows up again in production weeks later, past the point where anyone remembers this code was ever touched.
Hyrum Wright's observation about APIs generalizes past APIs: "With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody." A legacy function with enough callers behaves the same way. Its documented behavior, if it has any, is not the whole story. Its actual, current, exact behavior is what every caller has learned to depend on, on purpose or by accident, and that includes the weird branch you're eyeing.
How Do You Get AI to Propose Hypotheses Instead of a Confident Answer?
You ask for more than one hypothesis, ranked by how much evidence you've actually given it, and you explicitly forbid it from picking a favorite before you've checked anything.
Role: You are a careful engineer doing archaeology on unfamiliar legacy code,
not explaining a well-documented system.
Task: Below is a function with a branch or check that looks unusual. Propose
2-3 plausible reasons this code could exist in its current form, ranked by
how much the pasted evidence actually supports each one. For each hypothesis,
state exactly what additional evidence (a commit message, a ticket, a test,
a log line) would confirm or rule it out.
Format: One numbered hypothesis per entry: the hypothesis, the evidence for
it so far, and what would confirm or deny it. Do not recommend removing or
keeping the code. That decision comes later.
[PASTE THE FUNCTION OR BLOCK]
Notice what this prompt doesn't ask for: a verdict. That's deliberate. The moment you ask "should I remove this," you're asking the model to guess past the point its evidence actually supports, and it will, because a plausible-sounding answer is exactly what it's built to produce. Asking for falsification criteria instead keeps the model useful for the part it's actually good at, generating candidate explanations fast, without asking it to do the part it can't: checking them against a history it never saw.
How Do You Test a Hypothesis Instead of Trusting It?
You go find the specific evidence the previous step named, then you feed that evidence back and ask the model to update its confidence, not repeat its guess.
Role: You are re-evaluating your own earlier hypothesis against new evidence,
not defending it.
Task: Earlier you proposed this hypothesis for why the code below exists:
[PASTE HYPOTHESIS]. Here is what I found: [PASTE COMMIT MESSAGE, TICKET,
TEST, OR LOG]. State whether this evidence confirms, contradicts, or is
unrelated to the hypothesis, and update your confidence level accordingly.
If it's unrelated, say so plainly rather than stretching it to fit.
[PASTE THE FUNCTION AGAIN]
[PASTE THE EVIDENCE]
This is a small habit with a large effect: git log -p and git blame on the exact lines in question, a ticket-tracker search for the function's name, a test that already exercises the branch, all of these are cheap to gather and expensive to skip. A hypothesis with real evidence behind it is a fact you can act on. A hypothesis with none is still just the first guess, no matter how many times you've now asked the model to restate it.
How Do You Find the Load-Bearing Parts of a Legacy System?
You map every caller before you decide anything is safe, because "load-bearing" isn't a property of the code, it's a property of who depends on it and how.
Role: You are assessing whether a piece of legacy code is safe to change,
using only what you can verify from search results, not assumption.
Task: Given the function below and the search results showing every place
it's referenced in the codebase, list each caller, what it appears to
depend on from this function's current exact behavior (not just its stated
purpose), and flag any caller whose dependency looks like it relies on an
edge case rather than the function's obvious main behavior.
Format: A table: Caller | What it depends on | Edge case or main behavior?
If you weren't given all the callers, say so explicitly rather than
assuming completeness.
[PASTE THE FUNCTION]
[PASTE GREP/SEARCH RESULTS FOR EVERY REFERENCE]
Load-bearing code is rarely the part that looks important. It's often the part that looks like an afterthought, because Hyrum's Law applies whether or not anyone intended a behavior to be depended on. Something built for one caller can quietly become mandatory for six others over years, with nothing in the code itself flagging that it happened. A call-site map is how you catch this before a change does. Once you've built one, a genuinely useful AI code review on the eventual diff is the natural next gate, checking the change against exactly the callers this map surfaced.
What Should You Decide Not to Touch?
Sort every piece of legacy code you're investigating into one of three buckets, and only act on the first one.
| Bucket | What puts it here | What to do next |
|---|---|---|
| Confirmed safe to change | A hypothesis backed by real evidence (commit, ticket, test) that the code is genuinely unused, redundant, or superseded | Proceed to refactoring, with characterization tests as the safety net |
| Confirmed load-bearing | A call-site map or test showing real, current callers depend on this exact behavior, edge cases included | Leave it alone, or change it only alongside every dependent caller, deliberately |
| Insufficient evidence | You have a plausible hypothesis but no commit, ticket, test, or call-site map confirming or denying it | Go get the evidence. Do not act on a plausible-sounding guess |
That third row is where most legacy-code mistakes happen, because it's the easiest one to talk yourself out of. A hypothesis that sounds right, delivered fluently by a model that read the function carefully, feels like enough to act on. It isn't. The bar for the first bucket isn't "nothing contradicts this story." It's "something specific confirms it."
What Does a Full Archaeology Pass Look Like End to End?
In practice, this compresses to four moves, repeated for each piece of legacy code you're actually worried about, not the whole file at once: paste the odd code and ask for ranked hypotheses with falsification criteria; go find the specific evidence each hypothesis calls for, in git history, tickets, tests, or logs; feed that evidence back and get an updated confidence level instead of a repeated guess; then map every real caller before sorting the code into one of the three buckets above.
None of this is fast, and it isn't meant to be. It's meant to be falsifiable at every step, which a single "explain this and tell me if it's safe to remove" prompt never is. The output of a good archaeology pass isn't an explanation you find satisfying. It's a specific, checked claim: this exists because of this ticket, these three callers depend on this exact behavior, and here's the evidence for both.
When Are You Actually Done, and When Do You Move to Refactoring?
You're done with archaeology once every hypothesis you're relying on has real evidence behind it, not just a confident sentence, and every caller that touches the code you're about to change has been found and accounted for. That's a different, narrower bar than "I understand what this does," and it's the one worth holding yourself to before you write a single line of change.
Once you've cleared it, refactoring is a separate phase with its own discipline: build a safety net before you touch anything, change one named thing at a time, and demand a reviewable diff instead of a rewrite. 25 AI prompts for refactoring legacy code safely picks up exactly there, organized by transformation, with the characterization-test-first approach this post assumes but doesn't itself cover. Archaeology tells you what's true and what's load-bearing. Refactoring is what you do once you actually know.
Stop rewriting prompts. Start shipping.
Works with ChatGPT, Claude, Gemini, Grok, Midjourney, Ideogram, Veo3 & Kling. 4.8★ on the Chrome Web Store.
Create An Account