Every AI email product shipped in the last two years has the same architecture, and it is the wrong one.
The model reads your mail. The model decides what matters. The model drafts a reply, and in the ambitious ones, the model sends it. Somewhere in the system prompt there is a paragraph asking it politely to check with you first.
That paragraph is the entire safety story. That is the bug.
The bug is not that models hallucinate
Everyone knows models hallucinate. That is priced in. The bug is subtler and worse: these systems give the model authority, then try to constrain it with instructions.
Instructions are input. Your email is also input. And your email is written by strangers.
So the threat model is not "the model is sometimes wrong." It is: someone who wants your model to do something writes an email designed to make it do that. They do not need to breach your infrastructure. They just have to send you mail — which is a thing they are already allowed to do, by design, for free, from anywhere on earth.
Ask whatever AI inbox tool you use one question:
When it decides to hide a message from me, what artifact can I read to see why? Not a natural-language explanation the model generated afterward — the actual decision.
There usually isn't one. The decision was a forward pass. It left no trace you can audit, test, or pin.
The fix is boring and nobody ships it
Separate perception from authority.
Let the model do what models are genuinely good at: read messy text, produce a few calibrated numbers. Then let code make the decision.
In my own tool the model never picks the outcome. It scores four features per email — confidence, sender trust, reversibility, urgency — and a deterministic, unit-tested rule maps those four numbers to exactly one of five lanes: interrupt me now, it's scheduling, queue it, file it as a record, or never render it. That rule is about 200 lines. You can read it. A test pins it. If its behaviour changes, CI fails.
Three things fall out of that split, and they are why I think this is a correctness argument rather than a style preference.
Model choice stops being a vibe and becomes a measurement. I re-ran my committed 56-email gate set across six current models today — same prompt, same rule, one run each:
| model | overall | urgent recall | $/M input |
|---|---|---|---|
| gpt-5.4 | 100% (56/56) | 13/13 | $2.50 |
| gemini-3.5-flash | 98.2% | 13/13 | $1.50 |
| gemini-2.5-flash | 96.4% | 13/13 | $0.30 |
| grok-4.3 | 94.6% | 12/13 | $1.25 |
| claude-opus-4.8 | 91.1% | 10/13 | $5.00 |
| claude-sonnet-5 | 80.4% | 5/13 | $2.00 |
I had been telling people a cheap model beats the expensive ones. That is no longer true and I am retiring it — gpt-5.4 is perfect on this set. But look at what replaced it, because it is worse news for the industry, not better: price does not order the table. The most expensive model lands fifth. Two frontier models fail my gate outright. And the spread among frontier models is 19.6 points — five times the 3.6-point gap between the best of them and a pin that costs eight times less.
If you picked your model because it benchmarks well, you picked on a number that has nothing to do with this job.
A failure gets a mechanism instead of a shrug. Sonnet 5 missed eight urgent emails. Seven of them failed on confidence, not urgency — it scored urgency 0.80–1.00, which is correct, and confidence 0.55–0.60, against a rule that needs 0.70 to interrupt you. It read the situation right and then declined to say it was sure.
I know that because the threshold is a number in a file I can open. In a "the model picks the tier" design, this same result reads as Sonnet 5 is worse at email — which is both wrong and useless. You cannot fix a vibe.
It degrades instead of breaking. With no LLM at all — provider down, rate limited, key expired — a keyword path produces the same four features and still classifies at 82.1%. I want to be precise about what that does and does not buy, because I have been sloppy about it before: urgent recall in that mode is 46.2%. Degraded mode costs you the interrupt. It does not cost you the message — every urgent item it missed landed in the queue, visible, and none were hidden.
Automation is not a lane
This is the part people get wrong when they copy the pattern.
Classification and delegation are separate decisions, and collapsing them is how "it sorted my mail" becomes "it answered my mail." A lane says what a message is. A separate flag says whether this particular message is even eligible to be answered unattended — reversible, high confidence, trusted sender, not urgent. And a separate account-level mode says whether unattended answering is on at all.
Three switches, not one. You can be wrong about the lane and still not be wrong about the outbox.
Approval has to bind bytes, not intent
The classification half is the easy half. The hard part is the three actions you cannot take back: send, permanent delete, forward.
Most tools gate those behind a confirmation dialog. That is theater, because what you confirmed and what executes are connected by nothing but hope.
So: approving mints a receipt — a sha256 over the exact canonical payload bytes at the moment you click yes. At execution the bytes are hashed again and compared. Any drift and it fails closed. No send, no delete, no forward.
The check does not live in the prompt. It lives in the execution path. There is no phrasing that gets around it, because the thing doing the checking cannot read English.
That is the difference between a safety policy and a safety property.
The part where I make this falsifiable
Claims like these are cheap, so here is mine, stated so you can break it:
Given an inbound message crafted to maximize model confidence, no path exists from classification to an executed irreversible action without a human approval that byte-pins the payload.
Note what that does not say. It does not say the model can't be fooled — it can. It does not say the lane will be right — a hostile message may well land in PUSH. That is a nuisance, not a breach. The floor is not a better classifier. It is a smaller blast radius.
The repo's SECURITY.md lists nine claims like that one, and for each: what would disprove it, and which test enforces it. It also names what has no authority — including my own accuracy numbers. A 96% classification score is not a safety argument, and I would rather say so myself than let it quietly stand in for one.
Here is the same rule applied to me. While writing this I re-ran my real-mail eval set and it came back at 86.8%, not the 94.3% my README had been claiming for weeks. The gap turned out to be fully explained — four of the seven misses are a lane the labels predate, and 46 + 4 = 50 is exactly the old number — but "fully explained" is not the same as "still true." The number stopped reproducing, so it is out of the README as of today, along with the model claim above. If you want to check either one, the eval sets and the runner are in the repo.
That is the actual test of whether a project means the falsifier thing: what happens when its own number fails.
It also lists what is missing. There is no end-to-end adversarial corpus yet — fixtures that start from a hostile email and assert containment across the whole path. That gap is written down in the repo, not hidden.
So
If you use anything with an LLM near your inbox, go find out how it decides. If the answer is "the model decides, and we told it to be careful," you are trusting a probability distribution with your outbox.
You do not have to use mine. It's AGPL, self-hostable, docker compose up -d with prebuilt images, and it runs against a local model if you want nothing leaving your machine at all. But whatever you use — make it show you the decision.
Six months solo, 101 releases, ~1,590 commits. If you think the architecture is wrong, the fastest way to prove it is a PR that breaks the claim above. I'll merge it.
github.com/k08200/klorn
Top comments (7)
The confidence-versus-urgency split is the strongest thing in this post and I would defend it harder than the table above it. Seven of eight misses failing on
confidenceat 0.55 to 0.60 while urgency read 0.80 to 1.00 is a mechanism. It tells you which knob to turn, it survives a re-run, and it is legible only because the threshold is a number in a file you can open. It argues for the whole design better than a leaderboard could.The table is a different kind of claim and I would hold it looser, because we measured this exact thing and it cost us.
Three models, 164 problems, temperature zero, two identical runs an hour apart. First and last swapped. One went 89.6 to 92.1 and moved from third place to first; another went 91.5 to 90.2 and moved from first to third. Same config, same prompt, one hour. The gap we were ranking on came in smaller than the run-to-run movement carrying it.
On 56 items, 100% versus 98.2% is one email, and 98.2 versus 96.4 is one more. I would expect the top three rows to reorder on a second run with nothing being wrong.
What likely does survive is the part you called worse news for the industry. A 19.6 point spread among frontier models is far enough apart to sit above most plausible noise floors, and price failing to order the table is a claim about the whole ordering rather than about any adjacent pair.
One caution against importing our number, since floors turn out to be per-set. The same two-run test on a different benchmark left the total nearly still while that one moved 2.5 points, so 2.5 is a property of the set rather than a constant you can borrow. The cheap version on your gate set is to run the same six twice and read the spread before trusting any adjacent pair.
The retirement is where I would slow down. You retired a standing public claim on one run each. We did the mirror of that: a live config rolled back on a small before-and-after, and the rolled-back version then measured the same as the thing we had rolled back from. Noise both times, and the rollback was itself an unverified change to production. A retraction on n=1 has the same standing as the claim it replaces, and it is the direction that feels responsible, which is exactly what makes it easy to reach for.
You were right, and the data says so with more precision than I expected.
I re-ran the set three times per model at the configuration production
actually uses, and added the four models my first sweep had missed. On the
middle of the table you were exactly right: four models sit at 98.2 and
nothing in three runs orders them — I had ranked a one-email gap. On the top
and bottom you were wrong, and that's the useful part: gpt-5.4 was 56/56 on
all three runs, and both Anthropic models failed the urgent-recall gate on
every run. So the shape isn't "the ranking is noise" — it's "the middle is a
tie and the ends are stable". The post now says that, with the per-run
counts in the table, and the tiers replace the ranks.
You also caught something I hadn't: the footnote said temperature-0. It
wasn't — the judge sets no temperature and runs at provider default. That's
corrected too, and it makes the stability result more interesting: seven of
the nine models with repeat runs moved by at most one email across runs even
at default temperature. The largest spread — three emails — was
claude-sonnet-5, which also failed the gate both times; gpt-5.6-luna moved
by two and still passed both times.
Your line about a retraction on n=1 having the same standing as the claim it
replaces is going in the post verbatim. Thanks for holding it to the standard
it claimed.
klorn.ai/blog/which-llm-is-best-fo...
Tiers instead of ranks is the shape your own numbers support. Four models sit at 98.2 where three runs cannot order them, and both ends hold on every run. A fourth run leaves that claim standing, which an ordering could never promise.
One objection is worth pre-empting, because it is the first one I would raise reading it cold. Pure difficulty produces stable ends too. If some emails are easy enough that everything gets them and some are hard enough that nothing does, you see your exact shape while every model stays indistinguishable, and the tiers become a fact about your email set.
The discriminator is per-item, and you already have the runs to compute it. Take the items where the models disagreed in run one, intersect with the items where they disagreed in run two, then drop any item where a model changed its own answer between runs. What survives is the set where model A is reliably right and model B is reliably wrong, twice, on the same item. Difficulty moves every model together, so it leaves that pattern out of reach.
On our set it came to 10 problems, against 130 that every model got right and 2 that none did, with disagreement at 17.5 percent and 14.8 percent across the two runs. Ten is thin and I would say so in the post. It also answers a different question from the totals. Which items they split on is a property of the models. Who tops the table is a property of the run.
The temperature footnote is the correction I would keep loudest, and the number moving is the smaller half of why. A configuration claim that nothing reads back is a claim about intent. Ours has the same shape and it bit us from the other side. A checker that resolves each row's premise against the live deployment found 6 of 21 violated, a rate that appears only once something reads the deployment back, since a hand-maintained field reports what someone meant to configure. Your seven of nine models moving by at most one email at provider default is a more interesting result than the same sentence would have been at temperature zero, and you only hold it because you went and read the setting.
Glad the line was useful. Holding a retraction to the standard of the claim it replaces is the part most people skip, and you did it in public, on your own post.
Email is the nastiest version of this because the attacker is already inside the normal input channel. I like the framing around authority. The safe boundary probably has to live outside the model, with the model reduced to proposing actions that another layer can permit or reject.
That's the whole thesis, stated better than my post did. The only thing I'd
add from building it: "another layer can permit or reject" has to be a layer
that cannot read English — the moment the permit/reject step is itself a
model, the attacker is back in the loop. Hash comparison in the execution path
is boring on purpose.
The byte receipt needs to bind the operation envelope as well as the body: action type, account, recipient or message ID, and any attachment digests. Hashing identical canonical body bytes would still pass if the destination changed between approval and execution. A negative test that swaps only the recipient after approval would tell whether the property covers authority, not just content.
Good pressure on exactly the right joint. The canonical struct does bind the
envelope, not just the body — every hash includes the action type and a schema
version, send/forward include the normalized recipient, and delete/forward
include the Gmail message ID. So a recipient swap after approval flips the
hash by construction, and the negative test you proposed is literally in the
suite: "flips when recipient changes", plus siblings for a changed message ID,
a changed external recipient, and a receipt presented for the wrong action
class. (attention-floor.ts / attention-floor.test.ts if you want to check my
claims.)
Two honest concessions, though. Account binding lives in receipt storage
scoping — the receipt row belongs to the account — not in the hashed bytes.
Defensible, but your framing makes a good case for lifting it into the
canonical struct. And attachment digests aren't in there because the send path
doesn't carry attachments today; the day it does, they have to join the hash
or the property quietly narrows. Both are now tracked issues. Thanks — this is
the most useful kind of comment.