ka
Khairul Azharsenior software engineer
All writing
InsightsAug 16, 2026 · 4 min

I Don’t Let My AI Review Its Own Code

An AI agent built me a page where the save buttons did not exist, and every automated check said ship it. The rule that caught it: the agent that builds is never the agent that verifies. Execution contracts, proving commands, isolated test state, and a month of receipts.

KA
Khai
Senior Software Engineer
I Don’t Let My AI Review Its Own Code

Last week an AI agent built me an admin page that looked perfect. Two-column layout, sticky sidebar, everything on the design system. Lint clean, build green, confident completion report. One problem: the save buttons did not exist. Not hidden. Absent from the DOM. The page could not save anything, and every automated check said ship it.

It got caught for one reason: the agent that built the page is never the one that verifies it. That is the rule my whole AI workflow hangs on, and I want to explain why it does more work than any other rule I have tried.


The setup

I run two agents in one terminal. Claude Code acts as the orchestrator. It plans, writes task contracts, reviews diffs, and drives the running app. A second agent in a sibling pane does the implementation. The orchestrator never writes the feature code; the builder never judges its own work.

Every delegated task carries an execution contract with four parts:

  • Boundary. The exact files the task may touch. Nothing else.

  • Dependencies. What must exist before this starts.

  • Acceptance criteria. Definition of done in plain words.

  • Proving command. The exact command and expected output that demonstrates it works.

A task missing any of the four does not get delegated. Vague contracts produce confident garbage. Sharp contracts let the builder loop autonomously and give the verifier something falsifiable to check.


What the second pair of eyes actually caught

One month of receipts from a single project:

  • The missing save buttons. A collapsible-group component gated its content on a state that was permanently false for non-toggleable groups. The builder’s report was glowing. Clicking the actual page found a publish panel that was an empty shell.

  • The untested branches that mattered most. An identity resolver shipped with its exact-match paths proven, while its fuzzy-match and merge paths (the entire reason the feature existed) had never executed once. The verifier demanded scenarios for them; the merge path then needed a fix.

  • The “done” that leaked data. A task to remove raw IPs from API responses was reported complete. An independent sweep found a second endpoint still selecting and grouping by ip_address.

  • The spreadsheet bug in the safety feature. A CSV formula-injection guard quoted every cell starting with a dash, turning every negative number into text that silently drops out of SUM(). The guard was protecting people from their own totals.

None of these were caught by lint, types, tests, or the builder’s self-review. All of them were caught by a different context asking: prove it.


Why builders can’t verify themselves

It is not that models are careless. It is structural. A builder verifies against its own mental model of the task, the same model that produced the bug. If it misunderstood the framework, its test encodes the same misunderstanding and passes by construction. The report reads “verified” and means “consistent with my own assumptions”.

A separate verifier has different failure modes. It reads the diff cold. It runs the proving command fresh and reads the full output. Most importantly, it uses the thing the way a user would. It clicks the button, loads the page at phone width, publishes the post and looks at what actually renders. Half my catches came from driving a real browser, not from reading code.


Verification has its own toolbox

  • Isolated state. Every destructive or data-writing test runs against a copy. The live database is never the test bed. Copy the SQLite file, point the test server at it, throw it away.

  • Real request paths. Spoofed IPs to dodge rate limits, emptied notification tokens so tests do not spam my own Telegram, but the actual HTTP routes end to end. Never a re-implementation of them.

  • Row-count assertions. “The delete worked” is checked as: this table went from N to N minus 1, every child table went to zero for that id, and every other row survived. Absence of errors proves nothing.

  • Correction passes, not do-overs. Findings go back to the builder as minimal-diff instructions pinned to files and lines. Never “please fix everything”, which invites a rewrite and a fresh set of bugs.


The uncomfortable takeaway

AI made writing code cheap. It did not make knowing the code works cheap. That cost did not disappear. It moved. The bottleneck in an AI-assisted workflow is verification, and pretending otherwise just means shipping the verification debt to your users.

So split the roles. Give the builder a sharp contract and full autonomy inside it. Give the verifier the running app, the proving commands, and permission to say “not done”. The friction between them is not overhead. It is the entire quality mechanism.

My AI does not review its own code for the same reason I do not review mine before a release: not because it is incapable, but because it is the one party guaranteed to agree with itself.

Next essay

Your Cache Header Is Lying to You

I published a post and my own admin said DRAFT for an hour. The culprit: a global Cache-Control header commented “prevent caching of sensitive content” that did the exact opposite, plus next.config headers overriding route handlers, and hard refreshes that save nothing.

ka

I design and ship resilient mobile platforms and the backends that keep them honest.

© 2026 · Privacy · v4.2.0 · commit 8a3f12c