Refactoring a Codebase You Don’t Understand
... or: how to renovate the house while you’re still living in it
A while back I opened a project I had been proud of a month earlier, asked the AI for one small change, and watched it think for four minutes, touch nine files, and quietly break the login screen while fixing a button. I read the diff and realized something uncomfortable: I no longer knew what was in my own app. Neither did the AI, really. We were two people rummaging around in a dark attic, both pretending we knew where the light switch was.
That attic is where a lot of vibe-coded projects quietly die. We have talked about keeping things clean as they grow (there is a whole post on it, the messy kitchen one). But that advice assumes you started clean. This post is for the other case. The one where the mess already exists. Where you have five thousand lines you did not really read, spread across files you did not name, doing things you could not fully explain if someone put a coffee in front of you and asked nicely.
You do not need to become a software engineer to dig out of this. But you do need to stop doing the one thing that feels most natural.
Do not tell the AI to “clean this up”
Here is the move everyone makes, me included. You feel the mess, you get a burst of courage, and you type: “This codebase is messy, please refactor it and make it clean and modular.”
Don’t.
You have just handed a blindfolded person a chainsaw and pointed vaguely at the forest. The AI will do something. It will rename things, move code around, merge two files, split another, and hand you back a project that looks tidier and behaves differently in ways neither of you can see. Refactoring means changing how the code is organized without changing what it does. The instant you cannot tell whether behavior changed, you are not refactoring. You are gambling with extra steps.
So the whole game is this: make the change safe and small enough that you can always tell if you broke something. Everything below is in service of that one sentence.
The argument, up front, so you can hold me to it: a) you cannot fix what you cannot see, so make the AI map it first; b) you cannot refactor safely without a safety net, so build one before you touch anything; c) you move one small piece at a time and check after every move, never in one heroic session.
Make the AI draw you a map first
You cannot renovate a house you have never walked through. Before a single line changes, I make the AI give me the tour.
I open a fresh conversation (a clean desk, remember the token post) and I ask it to be a surveyor, not a builder:
Do not change any code. Walk through this project and give me a plain-English map. What are the main files, what is each one responsible for, and which files depend on which? Point out anything that looks like it does more than one job, and anything that looks duplicated or dead. Write it so a non-technical person could follow it.
The word that matters there is do not change any code. You want a report, not a renovation. The AI is genuinely good at this. It will tell you that app.py is secretly doing login, email, PDF generation, and the dashboard all at once, and that there are two functions that both format dates slightly differently because past-you asked twice and it forgot the first one (this is exactly the near-duplicate mess we covered before).
Save that map. Drop it into your agent file as an index (what you should really have). Now you and the AI are looking at the same drawing of the house instead of each guessing where the walls are. This alone will make every later step calmer.
Build the safety net before you touch anything
Before you change a single thing, you need a way to know, automatically, whether the app still does what it did five minutes ago.
That way is tests. Not fancy ones. We have a whole post on making the AI write your tests, so I will keep it short here: you want the AI to write tests that capture what the code currently does, bugs and all. These have a name, characterization tests, because they pin down the current character of the app before you go rearranging its furniture.
Before we refactor anything, write tests that describe how this part of the app behaves right now. I am not asking you to fix anything. Just capture the current behavior so we will know immediately if a change breaks it. Then run them and show me they pass.
Now you have a smoke alarm. When you start moving code and something starts smoking, the alarm goes off the same minute instead of a week later when a user emails you.
And if setting up a test runner is a bridge too far right now (getting PyTest or Jest to even run for the first time can be its own miserable afternoon), do not let that stop you. There is a poor man’s safety net that works surprisingly well: write down the manual click path yourself. Three lines in a notebook. “1. Fill in the form. 2. Click Submit. 3. A green toast says Saved.” Do those exact steps before the cut, and do them again after. Even better, record a ten-second screen video of it working now, so you have proof of what “working” looked like before you touched anything. It is not automated and it does not scale, but for one room at a time it is more than enough. Match the size of the safety net to the size of the change.
And the other half of the net, the free one: commit to Git first. Git is your save game. We covered this. A clean commit before you touch anything means the worst possible outcome of the whole afternoon is git reset and a shrug, not a lost weekend. If you take one thing from this post, let it be: map, then net, then touch. In that order.
One seam at a time, and check after every cut
Now, and only now, do you actually change things. The temptation is to fix everything at once because you are finally motivated. Resist it. You renovate one room while the family still lives in the house. You do not demolish the whole thing and move everyone into the garden.
Software people have a lovely name for this: the strangler fig1. You grow the clean new version around the messy old one, one piece at a time, until the old mess can quietly be removed. No dramatic knock-it-all-down rewrite. Just steady replacement.
In practice, that means you pick one job that is currently tangled into a giant file and you ask for exactly that, and nothing else:
The file
app.pycurrently handles login as well as five other things. Move only the login logic into its own file,auth.py, without changing what it does. Do not touch the other features. When you are done, run the tests and show me they still pass.
One job. One move. Then you run the tests. Green? Commit it, that room is done. Red? You know precisely what broke, because you only changed one thing, so either you fix that one thing or you git reset and try a smaller cut. Then you do the next seam. Login, then email, then the PDF thing, then the dashboard. It feels slow. It is not slow. It is the only speed that does not end with you rummaging in the dark attic again.
Now, a specific thing that will happen the first time you move code into a new file, so you are not blindsided: the app breaks with a wall of red text about ModuleNotFoundError or something being “not defined.” This is normal and it is not a real bug. When login logic moves from app.py to auth.py, every other file that used to reach for it is now pointing at the wrong address, and the AI, in its excitement, often forgets to update all of them (or wires them up in a circle, where two files each wait for the other to load first). The red text is your friend here, it is telling you exactly which door no longer opens. So add this to the request every single time: “After moving the code, check and fix every import statement in both the new file and the original file, and confirm nothing is importing in a circle, before you tell me it works.” Say it up front and you skip the whole tantrum.
One more, if the file you are cutting into is genuinely huge. Asking an AI to refactor a 2,000-line file inside the chat is asking for trouble, this is the clean-desk problem from the token post wearing a different hat. It may truncate the file, quietly drop a helper function it did not think you needed, or lose the thread halfway down. So tell it not to try: “Only show me the specific functions that are changing. Do not reprint the entire file in the chat.” Let it edit the file directly and hand you back just the diff. Less for it to hold in its head, less for you to read, fewer places for it to lose a limb along the way.
A rule I hold hard: if a refactoring step wants to touch more than one concern, stop and split it. The AI will happily offer to “also tidy up while I’m in here.” That is how a five-minute rename turns into a two-hundred-file diff that murders your Saturday. Tell it no. One room.
The rhythm, and why it works
So the loop looks like this, and it is boring on purpose:
Map the house. Write tests for the room you’re about to touch. Commit. Move one thing. Run the tests. Commit if green, reset if red. Next room.
Notice what you never do. You never ask the AI to fix “the whole codebase.” You never let it change two things at once. You never move on without the tests telling you the app still works. You are not being timid. You are refusing to let a change happen that you cannot see.
Because that is the real spine under all of this, the same one under every post I write: the AI cannot read your mind, and it can only work with what is in front of it. A messy codebase it does not understand and you do not understand is the worst possible thing to put in front of it. So you do not hand it the whole haunted house and hope. You hand it a map, a smoke alarm, and one room at a time. You shrink the problem until it is one the AI can actually solve, and one you can actually check.
You made the mess with a machine that forgets. You clean it up with a machine that forgets, too. The difference is entirely in how small a piece you are brave enough to hand it.
So: map it in plain English, net it with tests, commit before you touch a thing, then move one seam at a time and check after every single cut. That is the whole method. Slow, dull, and it will save the project you were about to give up on.
Named after an actual plant that grows around a host tree and gradually takes its place. Software borrowed the metaphor and, as usual, made it sound more violent than the gardening version. And in case it makes you feel better: this is not a beginner’s shortcut. It is how the likes of Netflix and Amazon migrated enormous legacy systems without ever taking the service offline. You are using an enterprise-grade strategy, just pointed at your one messy app.py instead of a global streaming platform.



