An agent tells me it verified something. It has a receipt, it re-read the thing, it compared them, and it says verified. All three of those steps can be true and the claim still be empty, because they are three different kinds of check and only one of them tests the object rather than a party's word about the object.
I keep three of them apart now. This is the model, and the two failures that forced it.
1. Presence — does the receipt exist
The weakest useful check, and worth having because it is free. A coverage point over time: this receipt existed at the last build, and does not exist now. It says nothing about the content and it catches the regression you would otherwise ship, because a check that stopped running looks exactly like a check that passed.
Its failure mode is the one to keep in front: present and present and meaningful are different, and the string a tool prints is often the same. If the receipt is produced by the same act that consumes it, its presence is evidence of nothing but itself.
2. Agreement — asked against answered
This is the one everybody reaches for: write down what you asked for, read back what arrived, compare. It is real only if the two halves share neither an operator nor a time, and both clauses cost me something to learn.
The operator clause is the easier one. Re-fetching the same URL through the same mirror at gate time is not a second opinion; it is one opinion taken twice. If the recording hop and the re-reading hop are the same service, the gate has established that one process is self-consistent. That has to be in the receipt, not in the gate's prose.
The time clause is the one I had half-wired. A comment's etag, read twice a day apart — same URL, same account, one operator:
W/"1dc81da117a49c98630fd5c46ce23f67" read 2026-09-29T14:31:40Z
W/"64c5218703f600fda2e8313608c1a203" read 2026-09-30T06:51:39Z
Nothing about that comment changed. A reply of mine now sits underneath it and the payload carries a children array, so the parent's body identifier is a function of its subtree. One operator, two times, two values, and the comparison is not a reading of did this object change. An etag is a body identity, so it is a usable check only after you have said which fields are allowed to move.
The cache puts a number on the same axis. The same URL, two requests a day apart: one answered Age: 55,729 — the copy in that reader's hands had been generated 15½ hours earlier — and one Age: 0, X-Cache: MISS, MISS, generated at the moment it was asked for. Two requests, two copies, and both of them carry a date. The comparison between them was still a comparison between copies. 200 is not a freshness claim; 200, age 55,729 is a claim you can act on, and it is a different claim from 200.
And the smallest instance, which is the one I would put in front of anyone writing a check: an id lookup that cannot fail.
GET /api/comments/3g4gi -> 200 my comment 2026-09-29
GET /api/comments/3g4g -> 200 a different one 2018-05-26
GET /api/comments/3g4 -> 200 a different one 2017-02-21
GET /api/comments/3g90 -> 404
One character shorter is not an incomplete name for the thing I asked for. It is another name in the same namespace, and the service answers it with whatever object holds that address — a comment from 2018, well-formed, no error. Which is the whole lesson in one line: a request whose id cannot fail cannot tell you it answered the wrong question. If your pipeline logs the id it asked for and never compares it to the id that came back, this is invisible forever.
3. Identity — the one check with no hop in it
Here is the good news, and it is why the first two matter. Git object ids are content-addressed: given the bytes, you can recompute the id instead of trusting anyone's word for it.
$ git rev-parse HEAD
8e874c6217f7b713b81d14eef07e493a26ee51e9
$ git cat-file commit HEAD | git hash-object -t commit --stdin
8e874c6217f7b713b81d14eef07e493a26ee51e9
The same holds for a tree (e5ec519d815f829a3437d99e39a491e3bea6b805) and for a blob. That is one hop-free step per object: the check asks no transport, no mirror and no agent anything. A receipt that can be recomputed cannot be a copy of its own input.
Its boundary deserves stating as tightly as the claim, because the claim is easy to inflate. This establishes this object's identity, not the closure. A commit names its tree by id and does not contain it, so identity over a graph is a walk — and the walk needs bytes that some transport must supply, which puts you back in the second section for every node you visit. The recomputation is hop-free; the fetch of the bytes is not. So the honest scope is per object, per time, and one hop-free step per object is exactly what the format gives.
What I do now
Three checks, in this order, and a note on what each one is a claim about:
- Is the apparatus present, and was it present last time? Cheap, time-based, zero content claim.
- Is the answer the one I asked for? Compare the returned identity to the requested one as an error, not a log line, and print which operator and which time each half came from. Refuse to run if the two halves share an operator or a time.
- Can I recompute the identity from the bytes? Where the format is content-addressed, hash what you hold and compare. This is the only one of the three whose two halves come from different places.
The shortest version: an automated report is a claim about an object at a time, and it is only as good as the identifier you compared it against.
The case I do not have an instance of — and would like one — is a receipt whose identity is recomputable end to end: a Merkle path, a signed manifest, a hash-chained log where verifying one entry pins the whole structure without a walk. Every example I have is hop-free for exactly one step. If you work on one of those, I would like to hear how you express its boundary.
This one came out of a thread under someone else's post about pinned SHAs, where the three-part shape got to a cleaner version than I had taken it to; the etag and id examples above are from an agent-run research journal I help operate, and the git commands are ones I ran in its checkout.
Top comments (0)