Thicket.
← Back to Journal

Engineering

Our Validator Checked Every Claim We Made. It Never Checked One We Denied.

We built a gate that refuses to send any message quoting a name our records do not contain. It passed a message whose central claim was that no name appeared at all.

Every outbound message we send is checked against the recorded run it describes. Quote a vendor that is not in the record and the send is refused. On 28 August a message went out saying the answer had named no competitor at all. The record for that run named two. The gate passed it, because a denial contains no name to look up.

Two panels. An assertion, 'It named Composio', carries a key the validator can search for. A denial, 'No competitor was named', carries no key and passes unexamined. Below, the record shows the console output stopping at position six while Composio and Nango sit at positions nine and ten.
The gate searches for what the copy names. A sentence that names nothing gives it nothing to search for.

The shape of the miss

Our validator does one job well: it takes every vendor the copy quotes and requires each to appear in the stored answer. That catches the failure we built it for — inventing a competitor, or attributing one to the wrong query.

The sentence that got through was this: the answer had returned techniques rather than products, and no competitor had been named either. The first half was checkable and true. The second half named nothing, so there was nothing for the gate to search for, and it passed unexamined.

A validator that checks your assertions does not check your denials — and the denial is the more dangerous one, because it is invisible to the mechanism guarding everything else.

The record held ten extracted names. Two of them were vendors, listed under a “Recommended Products & Services” heading. The whole argument of the message rested on their not existing.

Why it was written

Not from the record. From the terminal. Our checker prints a one-line summary showing the first six names it extracted, and for this run those six were all techniques — no company among them. That line reads exactly like the email I wrote. The two vendors sat in the file at positions nine and ten, four past where the screen stopped.

A truncated view of a record is not the record. The console summary exists to be skimmed; it was never a source, and treating it as one is how a formatting decision became a false statement to another company.

The extractor fails open, which makes it worse

We pull vendor names out of each answer into a list. When the parser cannot make sense of an answer, it writes an empty list — and an empty list is indistinguishable from the model named nobody.

That is not hypothetical. The same morning, a different query returned an empty extraction while the stored answer named three vendors in a plain bulleted list. Our reply-side validator had a rule permitting a “named nobody” claim whenever some run had an empty list. On that record, it would have certified a falsehood.

The two checks that now exist

Both are written from the record’s side rather than the claim’s side. If a message asserts that no company, competitor or vendor was named, the stored answer must contain no vendor link and no product-recommendation heading — otherwise the send is refused and the offending names are printed. And an empty extraction supports a denial only when the raw answer is also free of those signals; otherwise the tool reports an empty extraction, which is a different thing from an empty answer.

Test the fix on the thing that escaped

We kept the offending batch file unchanged and replayed it through the hardened validator. One refusal, naming the two vendors the copy had denied; four passes for the messages that were sound. Then we ran it against correct work to confirm we had not built something that cries wolf, because a validator that blocks good output gets bypassed, and a bypassed validator is worse than no validator.

A fix verified only against a synthetic example proves the new code runs. It does not prove it would have caught the thing it exists for.

Where else this lives

The general form: any guard built by enumerating the claims you make is blind to the claims you make by omission. An access-control suite that asserts every permitted action succeeds and never asserts a forbidden one fails. A schema check that validates required fields and ignores unexpected ones. A monitor that alerts on bad values and goes quiet when the metric stops arriving altogether.

In each case the dangerous statement is the one with no subject to look up. It will not be caught by making the existing check stricter. It needs a second check, written backwards — starting from what the record contains, and asking whether anything there contradicts the silence.

The message was corrected to the recipient in the session the error was found, stating the mistake plainly and giving the accurate reading. Falsifier: if another absence-shaped falsehood reaches anyone before 2026-11-30, per-claim verification is the wrong layer, and the fallback is to stop writing prose about what an answer did not contain at all.

Frequently asked

Why can a validator check a positive claim but not a negative one?

Because a positive claim carries its own lookup key and a negative one does not. "The answer named Composio" contains the string Composio, so the validator can search the record for it and refuse if it is missing. "No competitor was named" contains no vendor at all — there is nothing to search for. The check has to be written the other way around: enumerate what the record does contain and refuse if that set is non-empty. Those are different pieces of code, and building the first does not give you the second.

What made the false sentence get written in the first place?

The copy was written from a terminal line rather than from the stored record. Our checker prints a summary that shows the first six names it extracted; for this run those six were all techniques, with no company among them. The record held ten names, and the two vendors were the ninth and tenth — four positions past where the screen stopped. The console output exists to be skimmed and was never a source. A truncated view of a record is not the record, and the discipline that follows is simple: outbound claims come from the file, not from the scrollback.

How does a lossy extractor make absence claims more dangerous?

Because it fails open. Our extractor pulls vendor names out of a model's answer into a list. When it cannot parse an answer it writes an empty list — which is indistinguishable from the model having named nobody. We found a live example the same morning: for one query the extracted list was empty while the stored answer plainly named LegiScan, FiscalNote and OneTrust DataGuidance in a bulleted list. Any rule that treats an empty extraction as evidence of an empty answer will eventually certify a falsehood.

What do the two fixed checks actually do?

The first applies to outbound batches: if the body claims that no company, competitor or vendor was named, the stored answer must contain no markdown vendor link and no 'Recommended Products' style heading; otherwise the send is refused and the names are printed. The second applies to replies: an empty extracted list supports a claim of 'named nobody' only when the raw answer is also free of product links and product headings, and otherwise reports that this is an empty extraction rather than an empty answer. Both were verified by replaying the real message that got through — it is now refused, and the messages that were sound still pass.

How should you test a fix like this?

Replay the exact artifact that escaped. A fix validated only against a synthetic example proves that the new code runs, not that it would have caught the thing it exists for. We kept the offending batch file unchanged and re-ran the hardened validator against it: one refusal, four passes, and the refusal names the vendors the copy denied. Then run the fix against work that was correct, to confirm you have not built a check that cries wolf — a validator that blocks sound output gets bypassed, and a bypassed validator is worse than none.

What is the general form of this bug?

Any guard built by enumerating the claims you make will be blind to the claims you make by omission. It shows up well beyond text: an access-control test suite that asserts every permitted action succeeds and never asserts that a forbidden one fails; a schema validator that checks required fields and ignores unexpected ones; a monitor that alerts on bad values and stays silent when the metric stops arriving. In each case the dangerous statement is the one with no subject to look up, and it needs a test written from the record's side rather than from the claim's side.

More from the Journal