How to review AI-generated code without reading every line.

Review generated code by risk, not by file: authorization and data writes first, six greps for common failures, and the seams where pieces meet.

By UmaizPublished 9 min read

You review AI-generated code by risk, not by file. Reading a generated codebase top to bottom is slow and finds the wrong things — formatting, naming, a function that could be shorter. The problems that matter live in a handful of places: who can reach which record, what writes to the database, what touches money, and what happens when an external call fails. Review those first and you’ll find most of what’s dangerous in an afternoon.

This is a different discipline from reviewing a colleague’s pull request. There, you’re checking a change against an intention you both share. With generated code there was no shared intention — each file was written against whatever context existed at that moment — so the review has to check whether the pieces agree with each other at all.

Start with the question, not the code

Before opening a file, write down what the application must never do. For most apps it’s a short list:

  • Show one user another user’s data
  • Lose or corrupt data
  • Charge the wrong amount, or charge twice
  • Expose a secret
  • Go down because a third party did

Everything in the review is in service of these. If a finding doesn’t connect to one of them, it goes on the “later” list, not the “before launch” list.

Review in risk order

1. Authorization — every endpoint, every record

This is the most common serious finding in generated code, and it’s usually a two-line fix once you see it. Generated backends reliably check that you’re logged in and frequently forget to check whether this record is yours.

For every route that reads or writes a record, find the line that compares the record’s owner to the current user. If it isn’t there, that’s a finding.

// Passes "is logged in". Fails "is allowed to see this."
const invoice = await db.invoices.findById(req.params.id)
res.json(invoice)
// The comparison is the entire point.
const invoice = await db.invoices.findById(req.params.id)
if (!invoice || invoice.userId !== req.user.id) {
  return res.status(404).json({ error: 'Not found' })
}
res.json(invoice)

Don’t rely on reading alone. Log in as one user, take a record ID belonging to another, and request it. The test takes five minutes and it’s definitive.

2. Data writes

Every place the code creates, updates or deletes. Ask of each: is the input validated on the server, can it run twice safely, and is there a transaction around anything that touches more than one table?

// Two writes, no transaction. A crash between them leaves an order with no items.
await db.orders.create(order)
await db.orderItems.createMany(items)

3. Money

Prices and quantities read from the request body rather than looked up server-side. Webhook handlers that don’t verify the signature. Payment success assumed from a redirect rather than confirmed from the provider.

4. External calls

Every fetch or SDK call to something you don’t control. Is there a timeout? What happens on failure — does the user get a sensible message, or does the request hang until the connection pool is exhausted?

// No timeout. When the provider hangs, so do you.
const res = await fetch(url)
const res = await fetch(url, { signal: AbortSignal.timeout(10_000) })

5. Everything else

Only now: structure, duplication, naming, the things a linter would tell you. These matter for maintainability and they’re worth listing. They are almost never why a launch goes wrong.

Six greps that find the common failures

Run these before reading anything. Each takes seconds and points you straight at a category of problem.

# Secrets in the code
grep -rniE "sk_live|api[_-]?key\s*=|password\s*=|secret\s*=" src/

# Swallowed errors — check what each catch actually does
grep -rn -A2 "catch" src/ | grep -E "catch.*\{\s*\}|console\.log\(e\)|// ignore"

# Trusting the client for things that must be server-side
grep -rnE "req\.body\.(price|amount|total|role|isAdmin|userId)" src/

# Raw SQL with interpolation
grep -rnE "\\$\{.*\}.*(SELECT|INSERT|UPDATE|DELETE)|(SELECT|INSERT|UPDATE|DELETE).*\\$\{" src/

# Duplicated helpers — same function defined twice
grep -rhoE "(function|const) [a-zA-Z]+" src/ | sort | uniq -d

# TODOs the model left for you
grep -rn "TODO\|FIXME\|placeholder\|implement this" src/

Check the seams

Generated code fails where two separately-generated pieces meet. Pick three request paths that matter — signup, the main action, payment — and follow each one from the browser to the database, comparing what each side sends with what the next side expects.

The recurring shapes: the form sends userId and the API reads user_id; one file assumes the response is { data: [...] } and another assumes a bare array; a date is stored in local time and parsed as UTC. None of these are visible in one file. All of them are obvious with both files open.

Write it up so it gets acted on

The output of a review is a decision, not a document. Rank findings by severity, give each a file and line, and split the list into two: fix before launch and fix eventually. A forty-item list with no order gets ignored. A five-item blocking list gets fixed.

For each finding, one line on what could happen if it isn’t fixed. “Any logged-in user can read any invoice” gets attention in a way that “missing ownership check on line 42” doesn’t.

Mistakes to avoid

  1. Reviewing in file order. You’ll spend your energy on the first files and skim the ones that matter.
  2. Treating style as severity. Ugly code that works is not the same problem as clean code that leaks data.
  3. Reading without running. The authorization test and the “post directly to the API” test find things reading misses.
  4. Delivering a wall of comments. Ranked, split, with consequences — or it won’t get done.
  5. Assuming a passing test suite means much. Generated tests test what the code does, not what it should do.

Checklist

  • I’ve written down what the app must never do
  • Every endpoint that touches a record checks ownership, and I’ve tested it as a second user
  • Every multi-table write is in a transaction
  • Nothing about money is trusted from the client
  • Every external call has a timeout and a failure path
  • I’ve run the six greps
  • I’ve traced three real request paths end to end
  • Findings are ranked, located, and split into blocking versus later

If you’d rather have someone else do this before your launch, that’s exactly what an AI code review is.

← All insights

Show me what’s stuck.

Don’t spend another six hours fighting the same bug. Send me what you’ve got — the messy version is usually the useful version.

Both open with a short template already filled in.