Quality·9 min read

Review AI-written code safely

AI writes code fast, and fast is exactly the problem: a plausible-looking diff can hide a security hole, a silent data-loss bug, or a dependency that doesn't exist. Reviewing AI-written code isn't the same as reviewing a coworker's PR — the failure modes are different, and so is the craft. This is the playbook for reading what the model gave you before it reaches production.

The mindset: the model is a confident junior who never says 'I don't know'

The single most useful frame is this: treat every AI diff as a pull request from a talented junior engineer who has read all the docs but shipped to production zero times — and who will never, ever tell you they're unsure. Human juniors hedge ("I think this works but I'm not sure about the edge case"). The model states everything with the same even confidence whether it's dead right or hallucinating an API that doesn't exist. That means you cannot use tone or confidence as a signal, the way you unconsciously do with people. You have to verify the boring-looking parts too, because the model is equally fluent when it's wrong. Your job in review is not to check whether the code looks reasonable — it always looks reasonable. Your job is to check whether it is true.

Read the diff before you run it — and read it in the right order

The temptation is to accept the change, run the app, see it work, and move on. Don't. "It works" only tells you the happy path executed once on your machine; it says nothing about the auth check that got quietly loosened or the migration that drops a column. Read the diff first, and read it in dependency order, not top-to-bottom file order. Start at the data layer (schema changes, migrations, queries), then the business logic, then the UI. A bug in a Drizzle migration or a Supabase RLS policy is a hundred times more expensive than a wrong button color, and if you review UI-first you'll be fatigued by the time you reach the part that matters. For every file the model changed, ask the question it can't answer for itself: why did it touch this? AI diffs frequently include drive-by edits — reformatting an unrelated function, "helpfully" changing a default, deleting a line it decided was dead. Those unrequested changes are where regressions hide.

The five failure modes that are specific to AI code

Human bugs and AI bugs don't overlap perfectly. These five are disproportionately AI: (1) Hallucinated APIs — methods, npm packages, or config keys that sound right but don't exist; a package that doesn't exist is also a supply-chain risk if an attacker later registers that name. (2) Outdated patterns — the model's training skews toward older, more-represented code, so you'll get pages-router Next.js, deprecated Stripe API versions, or `useEffect` data-fetching when the current idiom has moved on. (3) Silently swallowed errors — `try/catch` blocks that log and continue, turning a hard failure into corrupt state. (4) Confident-but-wrong security code — auth checks that look right but compare the wrong values, or that check authentication (who you are) while forgetting authorization (what you're allowed to touch). (5) Plausible test theater — tests that assert on mocks, or that would pass even if the function did nothing. Learn to spot these five on sight and you catch most of what matters.

typescript
// Looks fine. It is not fine.
async function getInvoice(id: string, userId: string) {
  const invoice = await db.query.invoices.findFirst({
    where: eq(invoices.id, id),
  });
  // Authentication was checked upstream — but this never
  // checks the invoice actually BELONGS to userId.
  // Any logged-in user can read anyone's invoice by guessing an id.
  return invoice;
}

Make the machine check the machine

You are a slow, distractible reviewer. Tools are fast and tireless, so front-load them and only spend your human attention on what tools can't judge. Non-negotiable gate before you even read the diff: the code must typecheck (`tsc --noEmit`), pass the linter, and pass existing tests. A strict TypeScript config plus ESLint catches a huge share of hallucinated APIs for free — a method that doesn't exist won't compile. Run `npm audit` or your dependency scanner on any diff that touches `package.json`. Use a static analysis / SAST pass (many CI setups run something like Semgrep or GitHub's CodeQL) to flag injection and auth patterns mechanically. For anything with a real runtime surface, drive the actual flow with Playwright rather than trusting the model's claim that it works. The rule: never spend human review cycles on something a compiler, linter, or test could have caught. Save yourself for the semantic questions machines can't answer — is this the right behavior?

Trust boundaries: where AI code is most dangerous

Not all code deserves equal scrutiny, and pretending otherwise just means you review everything shallowly. Concentrate your attention where a mistake crosses a trust boundary — the places where wrong code becomes a breach or data loss, not just a bug. That's: anything touching auth and sessions (let Clerk or your auth provider own this; be deeply suspicious of hand-rolled token logic), any raw SQL or query built from user input, file uploads and path handling, anything that spends money (Stripe webhooks especially — verify the signature, and make handlers idempotent so a retsry can't double-charge), secrets and environment variables (the model loves to hardcode a fallback API key "for convenience"), and any endpoint that mutates data. A useful habit: grep the diff for the danger words — `process.env`, `dangerouslySetInnerHTML`, `eq(...id...)` without an owner check, `exec`, `any`, `// @ts-ignore`, `catch {}`. Each hit is a place to slow down.

Ask the model to review itself — but don't believe it blindly

A genuinely effective move: paste the diff back into a fresh session and ask a pointed adversarial question — "What are the three most likely bugs in this code? Where does it break under concurrent requests? What did you assume about the input that might be wrong?" A clean context with an adversarial prompt catches things the generating session was blind to, because the model that wrote the code is primed to defend it. This is cheap and surprisingly high-yield. But treat the output as a list of leads to investigate, not a verdict — self-review will also invent problems that aren't real and miss the one that is. It's a spotlight you point at suspicious areas, not a stamp of approval. The final judgment stays with you, because you're the one who understands what the software is actually supposed to do.

Small diffs, understood diffs, and the line you must never cross

The best defense against unreviewable AI code is to never generate unreviewable AI code. Ask for changes small enough that you can hold the whole diff in your head — a 60-line diff you fully understand is safer than a 600-line diff you skim. If the model produces a giant change, don't approve it faster; make it smaller. And hold one hard line: never merge code you don't understand. "It works and the tests pass" is not the same as "I know what this does." The moment you're accepting code because you trust the model rather than because you followed the logic, you've stopped reviewing and started gambling — and you'll find out the difference during an incident at 2am when you have to debug code no human ever actually read. If a diff is beyond your understanding, that's a signal to learn the area or shrink the change, not to click merge and hope.

Takeaways

  • Confidence is not a signal — the model sounds identical when it's right and when it's hallucinating, so verify the boring parts too.
  • Read the diff in dependency order (data → logic → UI) before you run it; 'it works once' hides the auth and migration bugs.
  • Learn the five AI-specific failure modes: hallucinated APIs, outdated patterns, swallowed errors, confident-wrong security, and fake tests.
  • Front-load machines — typecheck, lint, tests, and dependency/SAST scans gate the diff before you spend human attention.
  • Spend your scrutiny at trust boundaries: auth, raw SQL, money, secrets, uploads, and any mutation.
  • Never merge code you don't understand — 'passes tests' is not 'I know what this does.'