cd /news/developer-tools/i-don-t-review-ai-code-my-build-does · home topics developer-tools article
[ARTICLE · art-118343] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

I don't review AI code. My build does.

A developer built a 169-line Node.js verification script to automatically catch bugs in AI-generated code, addressing the impracticality of manually reviewing every line. The script checks for dead links, missing alt attributes, duplicate H1s, and other issues, and is integrated into the CI pipeline so failing branches never deploy.

read6 min views1 publishedSep 1, 2026

Originally published on indiecore.net.

There's a ratio I spent a while pretending not to know about. Claude produces a plausible

two-hundred-line diff in roughly a minute. Reading two hundred lines of plausible code

properly — not skimming for shape, actually checking it — takes me fifteen or twenty. That

gap is the whole problem with "never merge AI-generated code without reading it," which is

advice I agree with and could not follow.

For a few months I scrolled diffs, recognised the shape of things, and merged. It felt like

reviewing.

What eventually gave it away was the kind of bug getting through. None of them were subtle

logic errors that careful reading would have caught. They were dull, mechanical things: a

dead internal link left behind by a route rename, an <img>

with no alt

, a canonical URL

pointing at the wrong host, two cache rules that turned out to concatenate. Every one was

something a fifty-line script could find in milliseconds, and none of them were things I was

ever going to reliably catch by eye at eleven at night.

So I stopped trying to read faster.

scripts/verify.mjs

is 169 lines of dependency-free Node. It walks the built dist/

directory and exits non-zero on anything I've decided must never ship.

npm run check      # build + verify — the same gate CI runs

It runs on every pull request and the deploy job depends on it, so a branch that fails it

doesn't reach the internet. Not because I'm disciplined; because the pipeline is wired that

way. (That plumbing is in the Cloudflare Workers post,

along with the five traps that cost me an evening.)

The link and image checks are the whole idea in miniature. Resolve every site-absolute URL

against what's actually on disk:

/** does a site-absolute URL resolve to a real file? */
const resolves = url => {
  const clean = url.split('#')[0].split('?')[0];
  if (!clean || clean === '/') return fs.existsSync(path.join(DIST, 'index.html'));
  const p = path.join(DIST, clean);
  if (fs.existsSync(p) && fs.statSync(p).isFile()) return true;
  return fs.existsSync(path.join(p, 'index.html'));
};

for (const href of attr(html, /href="(\/[^"#][^"]*)"/g)) {
  if (!resolves(href)) fail(where, `dead internal link → ${href}`);
}

Nothing clever. It's the kind of check that's tedious for a person to do on every page of

every build and free for a machine to do forever.

I didn't sit down and write a best-practices linter. Each rule went in on the day something

got past me:

The check What it's remembering
exactly one <h1> per page
a generated template that emitted two
alt on every <img>
accessibility regressions Lighthouse caught days later
dead internal links a renamed route that left pages pointing at a 404
_redirects targets resolve
a redirect pointing at a page I'd since deleted
no placeholder or filler copy draft text left in a page, one merge from production
Blogger markup in output migration leftovers surfacing weeks after the migration
every page listed in sitemap.xml
new pages that silently never got submitted

Resist the urge to write the exhaustive rulebook up front. I tried; you end up with forty

rules, six of which ever fire, and the other thirty-four generate enough noise that you stop

reading the output at all. Which is worse than having no verifier, because now you also

believe you have one.

The rules I'd least want to lose guard things I would never notice by looking at the site,

because the site looks fine.

This one validates the IndexNow key file:

// scripts/seo-ping.mjs submits URLs under this key; the crawlers reject the
// submission unless the matching file is live at the site root.
const key = fs.readFileSync(KEY_FILE, 'utf8').trim();
if (!/^[a-f0-9]{8,128}$/.test(key)) fail('indexnow', `key is not 8-128 hex chars: "${key}"`);
else if (!fs.existsSync(p))         fail('indexnow', `missing key file /${key}.txt`);

Ask an agent to "add IndexNow submission" and you'll get a correct submission script and no

key file, because nothing in the request mentions one. It isn't being careless. It has no way

to know what the silent failure costs, and a missing key file fails silently by design — the

crawler just ignores you. Same story with app-ads.txt

, which is read by ad networks and

nobody else, and which is now parsed field-by-field at build time against the IAB spec.

Alongside the verifier there's a Lighthouse budget, and it isn't negotiable:

performance ≥ 90, accessibility ≥ 100, best-practices ≥ 100, seo ≥ 100

This catches the failure the verifier can't: slow drift. No single change makes a site slow;

twenty changes, each fine on its own, do, and you never see the moment it happens because you

were looking at diffs rather than at scores.

A real one: the grey I use for code comments in these posts failed WCAG contrast. It looked

deliberate. Honestly, it looked good. One-line fix, and I would never have found it by

reading a stylesheet — the budget found it on the pull request (commit d64bce0

, if you're

curious).

Performance measurement on shared CI runners is noisy, so the gate re-measures instead of

failing on one bad sample:

// Only performance is re-measured, and only when it misses.
while (scores.performance < BUDGET.performance && attempts < RETRIES) { … }

That detail matters more than it looks. A gate that fails at random gets ignored, then disabled, then deleted. Flaky checks teach you to click through failures, which is the exact

One thing I'd underrated. Compare:

  ERROR  /blog/index.html: dead internal link → /games/word-slot

with a generic "validation failed". The first I can paste straight into a session and get a

correct fix in one turn; the second starts a conversation. Good error messages were always

worth writing. They're worth roughly double now, because the agent reads them too, and a

precise message is the difference between a fix and a guess.

It has no opinion about whether the code is any good. It won't tell me a function is a mess,

that an abstraction is wrong, or that the feature was a bad idea in the first place. That's

still my job, and now it's the only reviewing job I have — which is a better trade than it

sounds, because the mechanical layer is where all the volume is.

It also can't tell me the agent touched something it shouldn't have gone near at all. That's

a different problem and I handle it differently; it's in

the blast radius rule.

The transferable bit isn't "write a verifier for your static site." It's the question I now

ask every time I catch something in a diff: could a script have caught this? When the answer

is yes, writing the script beats remembering the lesson, because I don't reliably remember

lessons and the script doesn't get tired.

Originally published at ** I don't review AI code. My build does.**.

The code is on GitHub: Both scripts, ready to drop in

── more in #developer-tools 4 stories · sorted by recency
── more on @claude 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/i-don-t-review-ai-co…] indexed:0 read:6min 2026-09-01 ·