How to audit an AI-generated codebase before you build on it
You have a product that was built quickly, it works, and you are about to invest in it properly. Before you do, you want to know what you actually own.
This is the checklist we work through. It is published in full because half of it you can run yourself in an afternoon, and a client who has done that arrives at the conversation knowing what they are asking about. We would rather have that conversation.
It applies to any inherited codebase. It applies more urgently to one assembled fast with AI assistance, because the failure pattern there is consistent enough that you know where to look first.
Before anything: can you run it
More often than people expect, the first real finding is that nobody can. The original author had a machine configured over weeks, and none of that is written down.
Get it running locally from a clean checkout, following only what is documented. Time how long it takes. If it cannot be done without asking someone, that is the first finding and everything else is blocked behind it — because a codebase you cannot run is a codebase you cannot change safely.
Then do the same for deployment. How does a change reach production? If the answer is that one person does it from their laptop, you have a continuity risk regardless of code quality.
Dependencies and licences
Fast to check and frequently the most urgent thing in the report.
- Run the package manager’s own audit. Note how many advisories are high or critical, and whether a patched version exists at all — an unpatchable dependency is a different problem from an unpatched one.
- How far behind is the framework? One minor version is maintenance. Two majors is a project.
- Is the language runtime still supported? An application on an end-of-life runtime stops receiving security fixes regardless of what you do to your own code.
- Check the licences. Generated code pulls in packages freely and occasionally one carries terms incompatible with a commercial product. Rare, and expensive to find late.
- Look for abandoned packages — no release in two years, unanswered issues. These become your code the day they break.
Authentication and authorisation
The category where a quiet problem becomes a serious one, so it comes before anything about code quality.
Pick a protected resource and request it directly, bypassing the interface, with a valid token belonging to a user who should not have access. If it returns the data, authorisation is not enforced on the server and the front end has only been hiding buttons. That single test tells you more than reading the auth module.
Then: are passwords hashed with a current algorithm, or stored in a way somebody chose for convenience. Do tokens expire, and can they be revoked. Are secrets in environment variables or committed to the repository — check the git history, not just the current files, because a key removed in a later commit is still in the history and still compromised.
The data model
The decision hardest to reverse, so it deserves more time than its share of the code.
Read the schema before reading any application code. Does it describe the business, or the first screen someone designed? Are relationships enforced by foreign keys, or only by convention in the application? Is there any index beyond the primary keys?
Then check for migrations. A project with no migration history cannot safely change its schema now that real data exists, and establishing one retroactively is its own piece of work.
Finally, look for the same fact stored in two places. Duplicated state is where the two copies eventually disagree, and once they have, deciding which is correct is a business question rather than a technical one.
Concurrency
Invisible in normal use, so you have to go looking.
Find the operations where two users could collide: anything that claims a limited resource, anything that increments a counter, anything that checks a condition and then acts on it. For each, ask whether the check and the action happen inside one transaction, or whether there is a gap between them. The gap is the bug.
Also look at background jobs. If one can be picked up by two workers at once, it will be, and whatever it does will happen twice.
Error handling
Search the codebase for its catch blocks and read what each one does. You are looking for the ones that log and continue, which means a failure the user was never told about and you will hear about later from a customer.
Then check whether anything is watching. Is there error reporting wired to somewhere a person looks, or does everything go to a log file nobody opens? A product with no error visibility is a product where your customers are the monitoring.
Tests
Coverage percentage is the wrong measure and chasing it produces tests written to raise a number.
Better questions: do the tests run at all, right now, on a clean checkout? Do they test behaviour, or do they assert that the code does what the code does — a suite of mocks verifying mocks is a cost with no benefit. Is anything protecting the paths where a failure costs money? And does anything exercise the system end to end, or only units in isolation?
A small suite covering the critical paths is worth more than a large one covering getters.
Behaviour under conditions that are not the office
Point the product at a large dataset and a poor connection, which is where most inherited software stops behaving.
- Load a list view with realistic volume rather than demo volume. Many are written with no pagination because nothing was slow with fourteen records.
- Throttle the connection and use the product normally. Watch for duplicate submissions, responses written into screens that have moved on, and anything that retries without being idempotent.
- Submit a form twice quickly. See whether you get two records.
- Check what the database is actually doing per request. A page issuing one query per row of a list is the single most common performance fault in inherited code.
What the report should say
An audit that lists observations is a document. An audit that ranks them by what they cost you is a decision tool.
Every finding should carry what it costs if nothing is done, what it costs to fix, and whether the cost of fixing it rises with time. Authentication problems rise steeply. A messy but working module may never need touching. Those two should not appear in the same section.
And the report should state clearly what is sound. A product can have a disorganised codebase and a correct data model, which is a far better position than the reverse, and knowing what not to touch is as useful as knowing what to fix.
Where this ends
Usually not in a rewrite. Most of what you find is either fine, cheap to correct, or safe to leave. The purpose of the exercise is to know which of those three each thing is before you commit a quarter to the product.
If you would rather not run it yourself, that is what we do. The written report is yours either way, including if you take it to a different team.
If you want someone to look properly
Send us the repository, or a login, or a URL. We will tell you what we find and what we think it needs, before anyone signs anything.
Related reading
- What breaks first in a vibe-coded app — the pattern this checklist is looking for
- Project Takeover and Modernisation — the audit as an engagement