We use AI assistants every day and we would not go back. We have also cleaned up codebases where a team went fast for four months and then spent six months paying for it. Both things are true, and the difference between them is not the tool — it is the discipline around it.
The failure mode nobody warns you about
It is not that the AI writes bad code. Bad code gets caught. It is that the AI writes plausible code — correctly formatted, sensibly named, idiomatic-looking code that handles the happy path and quietly does the wrong thing in the case you did not mention.
Plausible code slides through review, because review is pattern matching and the patterns all look right. Six weeks later you discover that the retry logic retries non-idempotent writes, or that the "cached" helper is re-fetching on every call, or that a permission check exists but is checking the wrong subject.
This is a code review problem that got worse, not a new category of problem. But the volume changed, and volume is what breaks review.
The one rule
Do not merge code you cannot explain to another person.
Not "code you read." Not "code that passed tests." Code you could defend, line by line, in a conversation. If you cannot say why the assistant chose that particular approach, you are not the author of that code — you are its first user, and you have shipped it to production.
Everything else here is a technique for making that rule cheap to follow.
Front-load the constraints
Most bad AI output is a specification failure. The model does not know your codebase's conventions, your performance ceiling or the module you are mid-migration on — unless you tell it.
The prompt that produces mergeable code looks like this:
Add rate limiting to the contact endpoint. Follow the existing middleware pattern in functions/api/contact.js. Use the Cloudflare KV binding we already have — do not add a dependency. Limit is 5 requests per IP per hour. Return 429 with the same JSON error shape the other handlers use. Do not touch the reCAPTCHA logic.
Four sentences of constraint, and the output is reviewable. Compare with "add rate limiting", which will produce a technically correct answer built on a Redis client you do not have, in a shape that does not match anything else in the repo.
The rule of thumb: if you would have to explain it to a competent contractor on their first day, explain it to the model. The overlap between those two briefings is close to total.
Point at the file to imitate
The highest-leverage instruction in an AI-assisted codebase is "match this." Give it the existing file that does the analogous thing and tell it to follow the same structure. You get consistent naming, consistent error handling and consistent testing for free, and the diff shrinks to the part that is genuinely new — which is the part you actually need to review.
This is also why a well-organized codebase compounds with AI assistance and a chaotic one degrades. The model imitates what it is shown. Show it a mess and it will produce more mess, faster, forever.
Delegate by category, not by size
What we hand over without hesitation:
- Tests for existing behaviour. Tedious, mechanical, high-value, and trivially verifiable — the test either exercises the thing or it does not.
- Mechanical refactors. Rename across 40 files, pull a hook out, convert a class component, migrate a call signature.
- Boilerplate and glue. Schemas, types, config, adapters, form validation, unfamiliar SDK plumbing.
- The first draft of anything you know exactly how to write and do not want to type.
- Explaining code you inherited. Genuinely excellent at this, and it costs nothing to be wrong.
What stays human:
- Data model and API design. The decisions you will be living with in three years.
- Anything touching auth, money or personal data. Assist, yes. Trust unreviewed, never.
- Debugging that requires knowing why the system is the way it is. The model can read the code; it cannot read the two-year-old incident that explains the weird timeout.
- Deciding what to build. Which remains the hard part, and always was.
Review the diff, not the story
A practical habit: read the diff before you read the assistant's summary of what it did. The summary is persuasive and it primes you to see what it says is there. The diff is the truth.
What we look for specifically in AI-authored diffs, because these are the recurring themes:
- Invented APIs. Methods that ought to exist and do not. Usually caught by the compiler; occasionally not, in dynamic languages.
- Silent error swallowing. A try/catch that logs and continues, where the correct behaviour was to fail.
- Dropped edge cases. Empty array, null user, first run, concurrent call. Ask directly: "what happens if this is called twice at the same time?"
- Quiet dependency additions. A new package to solve a problem your existing stack already solved.
- Comments that narrate. "// Loop through the items" — noise that survives forever. Strip them.
- Duplicated logic. The model did not know your utility function existed, so it wrote a second one. This is the slow poison — it does not break anything today.
Keep the codebase legible for the next prompt
There is a compounding loop here, and you choose which direction it runs.
Clear structure, consistent patterns, real names, a short document explaining the conventions — these make every subsequent AI interaction better, because the model has better material to imitate and more context per token. Sprawl, duplication and three competing ways to fetch data make every subsequent interaction worse.
Which means the argument for keeping a codebase clean just got stronger, not weaker. The old argument was "future humans have to read this." The new argument adds: "and every AI-assisted change you make from now on is a function of what is already in here."
Inherited a codebase you do not trust?
We take over existing projects — audit, stabilize, then ship features again. Including the ones that grew a little too fast.
Get in touch →