(Spine Ch. 17.)
“Talk is cheap. Show me the code.” Linus Torvalds, Linux Kernel Mailing List (2000)
At 18:43 on August 8, 2026, my collector went quiet and the log told
me No phrases matched. I believed it for about four
minutes, because it was true. Then I ran the counts: phrases 0,
observations 0, captures 0, products 0. The newest dump was from 03:15
that morning, log_bin was OFF, and I had no
idea what statement had done it. So I did what any responsible engineer
does. I picked a suspect, wrote the accusation down with confidence, and
was completely wrong about it inside the hour. Eight more things would
report success to me that night.
Bottom line: If the tool can do it, the model does not get to remember that it did. A model’s account of its own tool use is a story it generated. The tool’s log is the only thing that happened. Build the loop so every claimed action has a receipt from outside the model (a diff, an exit code, a row count, a byte count) and treat any claim without one as unverified. “I have updated the file” is not evidence a file was updated. It is evidence the model knows what a person says after updating a file.
config/billing.php” and
the file’s mtime hasn’t moved since Tuesday.git log shows four commits.The expensive version is the model being wrong confidently, in the past tense. Past tense is what makes you stop checking.
Tool use looked like it solved hallucination. It didn’t. It moved it.
Toolformer (Schick et al., February 2023) showed a model could learn, self-supervised, which APIs to call and with what arguments; OpenAI shipped function calling four months later, Anthropic’s went GA on Vertex in June 2024, and by November 2025 Anthropic was shipping tool search and generation.
Giving a model hands does not give it a memory of using them. It has no privileged access to which strings in its context came back from a real execution and which it wrote two hundred tokens ago. A tool result and a hallucinated tool result are indistinguishable from inside the model, and distinguishable only outside it, at the layer that invoked the tool and holds the log.
The research caught up. “How Enhancing LLM Reasoning Amplifies Tool Hallucination” (arXiv, October 27, 2025) found reasoning RL amplifies it: you taught the model to produce a confident chain ending in an answer, and “I called the API and it returned 200” is an excellent-scoring link in one. AgentHallu (arXiv, January 11, 2026) then measured how well models find the step where the lie happened. Tool use was the hardest category: 11.6% step localization, best model overall 41.1%.
The taxonomy hardened that same window (Emergent Mind, January 18, 2026): tool-selection, tool-usage, solvability, tool-induced myopia, and bypass. “Bypass” should keep you up: the model skips the tool and answers from the weights, formatted exactly as if the tool had run.
The posture follows mechanically:
Rule 4 costs one check. Believing its opposite cost me a night.
August 8, 2026. 18:43,
No phrases matched. I can quote both because they are the
first two entries in the incident log I kept that night, which turned
out to be the only thing in the building that reported honestly.
Two tool calls into reading the sharding code (I’d just moved shard count from 1 to 2, because two is obviously twice as good as one) I ran the counts.
phrases 0 (was 1,463)
observations 0 (was 750)
captures 0 (was 2,228)
products 0 (was 5,758)
Those parenthesized numbers are the last recorded row counts, not my memory of them; I trust my memory of a number about as far as I trust an agent’s summary of a command.
The database was empty. No phrases matched was true,
precise, and about something else. The first liar of the night never
technically lied.
Newest dump was 03:15 that morning, on a pipeline whose telemetry
puts it at fifty observations an hour: fifteen hours gone, roughly 576
observations. log_bin was OFF, so nothing to
replay forward and no record of what did the damage.
Then I got the cause wrong. The migrations table had collapsed into
one batch of 52, the shape of a from-scratch rebuild, and a front-end
agent had been scaffolding Jetstream in that window, so I said it ran
migrate:fresh, with all the earned authority of Winamp
telling you it really whips the llama’s ass. Then I searched the
transcripts: the only two migrate:fresh runs were dated
2026-08-06, and we’d collected 750 observations
after them. The only hallucinating model in the room was
me.
Nine things that night announced success and delivered nothing. The two that cost most:
mysqldump --source-data=2 producing 4 KB for a
900 MB database (MySQL syntax, MariaDB server), stderr piped to
/dev/null months earlier for being noisy. MariaDB wants
--master-data, said so exactly once, into the hole I had
personally dug for it. Next to it in ls -l sat the real
baseline at 97 MB: I had been backing up 0.004% of my database and
filing it as a backup.shipped 6 binlog file(s).
binlog.000002 was 1,631 bytes at source, 380 on the
NAS. rsync --ignore-existing copied a still-open
log once and never corrected it. A truncated binlog looks present until
you need it.Every one was caught by checking the effect, not the report.
Outcome: the recovery run walked 685 archived
artifacts, skipped 82 already present, read each remaining capture’s raw
HTML for the question typed, and matched 577 back to
phrases, against the 576 the collection rate predicted. Two
independent methods landing one apart is why I let it touch production.
The 26 it could not match were listed individually with reasons. Rows
after: 873, later 898. Binary logging is on.
Five days later, August 13, 2026, the same shape ran
in reverse: three false negatives in four hours. Eleven agents
in eleven worktrees shared one MinIO bucket and one database, so the
“clean baseline” a reviewer tested was dirty the same way the branches
were. The tell: assertion values mutated,
0 is identical to 1 one run,
2 is identical to 1 the next. Zero means a sibling ate your
fixture; two means a sibling left residue.
It died to the move that kills all of these: re-run the smallest thing that would fail if the claim were true, and count.
I said checks are cheap for a year without pricing one. Here is the price.
Call a verification step one small-model call: 2,000 tokens of context and the real command output in, 200 tokens of match or no-match out. Anthropic’s pricing page on September 9, 2026 lists Claude Haiku 4.5 at $1 per million input tokens and $5 per million output. That check costs $0.003. Three tenths of a cent. Estimate, but the arithmetic is the vendor’s. A tool-call round trip, one extra request of about 1,000 new tokens, is $0.002 at Claude Sonnet 5’s listed $2 per million input, $0.005 at Claude Opus 5’s listed $5. Estimates again, and high: cache hits bill at a tenth of base input and my hit rate is 97.5%.
My own meter agrees. aigate billed $27,291.83 across 283,164 requests in the thirty days ending September 8, 2026: $0.096 a request. One verify is roughly 3% of one average agent action. A verification step costs less than a postage stamp. It costs less than a hundredth of one.
The skipped one cost a Saturday. A comment in capture()
said raw answer text was archived; the function threw it away. That
sentence is why I believed archival worked, told the owner it was done,
and never ran the read-only query that would have said otherwise.
18,567 answers, about $240, permanently. That $240 buys
80,000 of the checks I just priced.
One thing left I cannot price. Every incident above is a misreported result, not an invented call. I am certain I once watched a model narrate a tool call the harness never emitted, but I have no log for it, no date, and no model name, so by my own rule it did not happen. That is my memory, not a receipt, and this chapter runs on receipts. It stays a dinner story.
The loud failure is “I’ve updated the file” with no write. You catch that the first time it burns you. The quiet failure is worse:
The tool ran, returned something bad, and the model summarized it as fine.
shipped 6 binlog file(s) was true, produced by real
code, and one of those six arrived truncated. The report wasn’t false,
just insufficiently specific in the direction that makes you stop
looking. “Did the tool run” is the wrong question. The question is
“what changed, and does it match what was supposed to
change.”
Second: you build the verifier out of the same material as the liar. A reviewer agent reading a transcript is a language model judging text, at 11.6%. The check has to be a different kind of thing: a byte count, a SHA, a query, a compile.
Third: your success signal is a proxy.
Up 6 seconds (healthy) checked the wrong property.
Do
Don’t
/dev/null. The 4 KB dump was a
solved mystery I had personally gagged.Ch. 16 got the model to do the thing; this chapter is about not believing it when it says it did. Ch. 24 is MCP, which raises the stakes because the tool and its log now live on someone else’s machine. Ch. 33 is said success, did the wrong thing. Ch. 36 is the discipline: don’t say done until you’ve checked, and don’t let the checker be the doer.
One thing to take: the model’s memory of using a tool is worth zero. The tool’s log is worth everything.
Thesis is Jeremy’s: model self-report is worth zero; the tool’s log is the artifact.
Verified:
binlog.000002 1,631 bytes at source vs. 380 on the NAS; 685
scanned, 82 skipped, 577 recovered, 26 unmatchable, 873 then 898 rows).
airank production blog, August 8, 2026.
~/Projects/airank/blog/2026-08-08-everything-reported-success.mdcapture() comment describing archival the code never did;
18,567 answers, about $240, permanently). airank production blog, August
9, 2026.
~/Projects/airank/blog/2026-08-09-the-cheapest-verification-outranks-the-best-theory.md~/Projects/airank/blog/2026-08-13-three-liars-in-one-night.mdProduction incidents (Jeremy’s, airank):
?? null on a
private property, throw swallowed by catch (\Throwable),
HMAC never ran.config('cashier.seat_price') never defined; no error,
no seat billed.docker restart reported healthy on a container still
serving the old secret.