Keeping AI Honest: Verification Habits That Scale
Generation was never the bottleneck — verification is. Seven habits that make checking cheap, the team convention worth adopting, the confident-and-wrong signature to recognise, and why unverified code is worse than it looks.
Hessam Rastegari
Senior Android Developer · 12 years shipping Android
TL;DR — The constraint in AI-assisted engineering is verification cost, so the habits that matter are the ones that make checking cheap: ask for checkable artefacts (tests, diffs, query results — never prose), make it quote the lines it relied on, don't let one pass write both the code and its only test, ask what would falsify it, grep every claim about your codebase, set the trust level before you start, and learn the confident-and-wrong signature. At team level: label generated code in the PR description — for routing, not shame. The compounding risk is that unverified code becomes context and gets laundered into convention.
The framing
There's a post earlier in this series arguing that AI's value collapses when verification is the expensive half of the task. This is the practical follow-on: given that, what do you actually do?
Not "check the output more carefully". That's advice that fails under deadline, which is the only condition that matters. What works is structuring the task so that checking is fast — then care becomes cheap enough to survive a bad week.
Seven habits
1. Ask for the thing you can check
Rank the outputs by verification cost:
| Output | Cost to verify |
|---|---|
| A failing test that now passes | Seconds — run it |
| A diff | Fast — read it |
| A SQL/query result | Seconds — re-run it |
| A table of facts | Medium — spot-check rows |
| Prose explanation | Expensive — you must already know |
Prose is the easiest thing to generate and the hardest to check, which is the worst combination available. When you catch yourself asking "explain how X works in our codebase", convert it: "write a test that demonstrates how X behaves". Now the answer verifies itself.
2. Make it quote
Before answering, quote the exact lines from the
files you're relying on. If you can't quote it,
say you're inferring.
Two effects. A model that must cite invents far less freely — the citation is a commitment. And when it is
wrong, you get a five-second grep instead of a bug discovered in week three.
The "say you're inferring" clause is doing real work. It gives an honest out, which is what you want: an answer flagged as inference is useful, an inference presented as fact is a trap.
3. Don't let one pass write both the code and its only test
If the same reasoning produced the implementation and the test, they agree with each other, not with reality. Both encode the same misunderstanding, and the green tick is meaningless.
// Fine: it writes the scaffolding.
// You write the assertion.
@Test fun `refunds are capped at the original charge`() {
val result = service.refund(charge = 5000, amount = 8000)
assertEquals(5000, result.amount) // ← yours
}
You supply the intent; let it supply the boilerplate. The division that matters isn't "who typed it" — it's who decided what correct means.
4. Ask what would falsify it
What input breaks this function?
What's the case where this is the wrong approach?
This converts a confident answer into a testable one, and it finds genuine bugs more often than it has any right to — the empty list, the concurrent second call, the timezone boundary.
It also works as a calibration probe. An answer with no failure modes at all is either trivial or overconfident, and it's rarely trivial.
5. Grep every claim about your codebase
"There's already a helper for this in :core:util." Ten seconds to check. It's either true — great, you
avoided a duplicate — or it's a fabrication that would have become one.
This is the single highest-value-per-second habit on the list, because claims about your code are exactly where a model has the least information and the most confidence.
6. Set the trust level before you start
Generated code you didn't read line-by-line is entirely fine for a throwaway script, and not fine for a module you'll own for three years. The mistake isn't picking the wrong level — it's picking it afterwards, when you've already got 300 working lines and reviewing them properly feels like waste.
Decide up front:
- Disposable — a script, a migration you'll run once, an internal tool. Skim for the destructive and the hard-coded; ship it.
- Owned — production code someone maintains. Read every line as if it came from a colleague whose work you don't yet know.
- Critical — payments, auth, data migration, anything with a security boundary. Read it twice, write the tests yourself, and have a second human look.
Say which one you're in before the first prompt.
7. Learn the confident-and-wrong signature
Hallucinations have a texture once you've seen enough:
- Oddly specific version numbers. "Since Room 2.6.1" — precise, plausible, frequently invented.
- API names that are almost right.
collectAsStateLifecycleAwareinstead ofcollectAsStateWithLifecycle. Close enough to look familiar, wrong enough not to compile. - "As of the latest version" — a phrase with no referent.
- Fluency rising as specificity drops. Longer, smoother, more hedged paragraphs are often the sound of a model with nothing concrete left.
- Confident answers about your private code. It has never seen your repo. Anything specific about it is a guess unless you pasted it.
The team habit: label it
One convention worth adopting: note AI-generated code in the PR description.
## AI use
Retry/backoff logic generated, reviewed line-by-line.
Tests written by hand.
This is not confession, and framing it as one kills it immediately. It's routing — the same information as "this file is generated protobuf" or "this is a mechanical rename". It tells the reviewer which lens to use and where to spend attention.
The failure mode to avoid: a policy that makes people hide it. If disclosure carries any social cost, you get less disclosure and the same amount of generated code, which is strictly worse than no policy.
Why unverified code is worse than it looks
The obvious cost is the bug. The compounding cost is that unverified code becomes context.
Today's unchecked answer is committed. Next month someone reads it as an example of how this codebase does things, and copies the pattern. The month after, you paste that file into a model as an example of your conventions, and it produces more of the same. An engineer joining next year infers a rule from it.
Errors get laundered into convention, and by then the provenance is gone — nobody can tell which decisions were considered and which were accepted at 5pm on a Friday. That's the real long-term risk, and it's not solved by better models. It's solved by checking things at the point of entry.
The honest limit
Verification cost never reaches zero. Every habit here reduces it; none removes it. Any workflow whose premise is that review has become unnecessary is one that hasn't hit its incident yet.
Which loops back to the thing that actually matters: the skill isn't verifying faster, it's choosing work where verification was already cheap. An internal tool you can check by looking at its output. A migration the compiler validates. A trace query whose result is on your screen. Those are the tasks where this all works — and recognising them is judgement no habit replaces.
The rule
Trust nothing you haven't checked, and pick tasks where checking is cheap. Ask for artefacts you can run, make it quote its sources, keep the assertion human, grep every claim about your own code, and decide the trust level before you start rather than after you're invested. The second half of that rule is the one that compounds.