← All notes
/
ReliabilityVerificationAI Engineering

Your Checks Are Lying to You

Run this in any repo with a gating script: ./check.sh | head -40; echo $?. If the gate fails, you still see 0, because without pipefail, $? is head's exit status, not the gate's. I measured it on a real gate in my own stack: run directly, exit 1; piped through a pager for readability, exit 0. The gate was correct. The call site threw its verdict away, and everything downstream recorded a pass.

I call this a fail-open check: a check whose “did not run” is indistinguishable from “passed.” Over one month I logged twenty-four named instances in one small stack of scripts, CI jobs, and agent pipelines. Not, I think, because the stack is unusually bad, but because I gave the class a name and started writing instances down.

Green is ambiguous by construction

Three from the same month:

  • A wrapper around a nightly embedding job logged success unconditionally and discarded stderr. The job had been dying mid-batch, at exit 0, behind months of healthy-looking logs.
  • A style lint reported clean on its first run because a comment line broke the grep over its own pattern file. Zero banned patterns were matched against anything. The check had not passed; it had not run. The recorded outcome was identical either way.
  • A link checker printed every finding to stdout and exited 0. Three downstream consumers read the exit code. None read the text.

The absence of a result gets recorded as the presence of a lesser one. A checker has three honest outcomes: found problems, found nothing after actually looking, and could not look. Most tooling gives it two exit states, so “could not look” gets folded into whichever side the error handling happens to land on. When it lands on green, the result is strictly worse than no check at all: a fail-open check produces a false record that closes the question, and the record is what stops anyone from looking again. A missing check, by contrast, is a visible gap. Anyone who asks “what verifies this?” finds nothing and knows the question is open.

The artifact is the camouflage

These survive for months because the failure produces an artifact, and the artifact conceals it. A test suite in that same stack wrote its “real build” case to the shipped deliverable instead of a fixture, which makes it an unreviewed production change wearing a green checkmark. Run to confirm an unrelated change was safe, it passed, and in passing silently rebuilt the deliverable against different inputs, evicting real content. Nothing failed, nothing warned; the output was a plausible, well-formed, freshly-dated file.

A curated export, built on a cloud machine, sat in the repo vouching for its build script, but an artifact is not evidence its producer runs here: the script used tomllib (Python 3.11+) against a local 3.9.6 and had never once executed locally. Dead at import. Its own test suite failed 9 of 14 cases the same way, and nobody read past the “5 passed” to ask out of how many.

This is not a tooling quirk; it is how oversight fails generally, because headcount measures effort applied, not failures found. Buffett's 2008 shareholder letter describes OFHEO, a regulator created to oversee exactly two companies, staffed with more than a hundred people with no other assignment. It published a glowing review of its own first decade and, on Buffett's telling, entirely missed that both companies had spent years misstating their earnings.

The ranking error underneath is universal: crashes and blank fields at the bad end of the severity scale, mostly-correct output at the good end. Ranked by expected damage, the order inverts. A crash routes immediately to a human who now knows something is wrong; the output that looks complete routes to acceptance, consuming exactly the attention budget that would have caught it.

The sneakiest variant: the check that cannot see the defect

The instances above fail by not running or not examining. The subtler ones run perfectly, against a scope that excludes the defect.

My own site's CI config carried .github/** in its paths-ignore list, alongside **.md and LICENSE. Reasonable on its face: editing a workflow file doesn't change the application. It meant the one change class able to delete a job, loosen a trigger, or drop a required check was the only class that merged with nothing run against it; a weakened gate and an intact one leave identical clean history, so nothing in the record would ever have surfaced it.

The same scope failure arises with nobody configuring it: a frontmatter parser whose regex read only the first item of every YAML block list computed a health metric over a universe 16% smaller than it claimed, and printed roughly 78% either way. A list API returned the first 20 items with has_more: true, then returned the identical page when handed its own continuation cursor. Nothing consumed the one field that contradicted the roster, and I nearly filed a report that four scheduled jobs had vanished. They were on page two.

And git log --since='7 days ago' --diff-filter=A --name-only on a fresh CI clone reported 1,618 files added that week; the true figure was 32. The clone was shallow, so its boundary commit appeared to add the entire repo at once.

The design-time test for all of these: if this control were wrong, what would tell me? If the honest answer is “the same green output I get when it is right,” you do not have a check so much as a green light wired to the wall.

The field guide

Rules that have held up, each earned by at least one instance above:

  1. Three outcomes, never two. Passed / failed / could-not-run, and could-not-run must be loud. This applies per item, too: an input your checker cannot parse must land in undetermined, never in a benign bucket like “no findings.”
  2. Assert the condition, not the reaching of the line. Emit success only after verifying the thing you claim, and match the invariant's shape to what consumers depend on. An embedding backfill satisfied its count-shaped post-condition (pending reached zero) while writing the same vector for forty different documents. A count cannot see a content defect.
  3. Every emitted marker needs a named consumer. A has_more flag, a truncation marker, a warning line: a signal nobody reads is worse than no signal, because it looks like coverage.
  4. Test the check against known-dirty input, from the position it actually runs in. Both checks I wrote to catch this class missed their own motivating cases on first run; only fixtures with known answers caught it. And a detector validated on a laptop can still be unrunnable at its scheduled call site; testing the detector is not testing the detection.
  5. A control's scope must include the control. Whatever decides what gets checked (a path filter, a sampling rule, a pagination default) is the best-hidden place for this defect.
  6. Spend verification on the fix. The patch that closes a fail-open is itself fresh, unverified check code, written under pressure with attention on the old defect. In my log, remediation builds are where new instances concentrate.

“So write more checks” is the wrong response. The fix for this class is a contract on the checks you have, not a headcount of new detectors, and every new detector is new surface for the same defect. “We have a runbook for this” is worse: a written policy with no enforcing mechanism is not a missing check, it is a fail-open one, occupying the slot where verification would report while readers take its existence as evidence the boundary holds. Often the strongest move is not a better check at all: restructure so the bad output cannot be produced. Will Larson describes catching an agent mis-forwarding alerts and, rather than adding an eval he knew would work, moving the filtering into a deterministic script so the agent never sees what it might mishandle. The check would have left the failure mode alive behind a gate; the restructure removed it from the system.

The same month produced two defects I deliberately did not log: they failed closed. Costly, but they never certified a falsehood, and logging them anyway would have blurred what the ledger measures.

A clean report from a check that cannot say “I could not run” is not evidence of anything. Build checks that cannot fail quietly, or their green eventually becomes the thing that hides the failure they were hired to catch.