Verify a session package
For the person handed a .treeship package. What to run, what each PASS means, and what it does not prove.
This page is written for the reader on the other side of the counter: the dispute analyst, the auditor, the acquirer's risk team, the customer's lawyer. Someone gave you a .treeship package, or a bundle file, or a receipt id, and told you it proves what an agent did. You do not need an account, a network connection, or Treeship's cooperation to check it. You need the free CLI and about ten minutes.
Every command and every line of output below is from a real run on a clean machine, against the shopping demo from Anthropic's commerce-agents reference: the shopping-demo transcripts with the released 0.30.0 CLI, the signature rows with a from-source build of 0.31.2. Nothing is simulated.
curl -fsSL https://treeship.dev/install | sh # or: npm install -g treeship
treeship init --name reviewer # a ship of your own; it signs nothing you will show anyoneWhat you were handed
A session package is a directory named <session-id>.treeship. The pieces that matter to you:
| File | What it is |
|---|---|
receipt.json | The session receipt: the list of every signed artifact by id and digest, the Merkle root over them, a timeline, and prose (a headline, a narrative). |
proofs/<id>.proof.json | One Merkle inclusion proof per artifact. |
merkle.json | The tree itself. |
approvals/ | Present when a human approval was spent in the session: the signed grant, the use records, the checkpoints. |
artifacts/ | The signed DSSE envelope of every sealed artifact, one file per id. Packages built before 0.31.2 carried only the approval grant and the actions that consumed it; for everything else the signed bytes lived in the producer's store, a bundle, or the hub. |
keys.json | The public keys the envelopes were signed with, by key id. The package names them. Whether to trust them is your decision, and Step 2 is where you make it. |
preview.html | The receipt page. Opens in a browser, verifies the Merkle structure client-side, no network. |
Two rows are the first thing to understand. Since 0.31.2 the package carries its own signed bytes, so package verify checks the signatures from the file you were handed. A package from an earlier release proves structure only; the default verifier fails it and says why, and --structural reports what such a package can still show. In both cases the package cannot tell you whose keys these are. The commands below cover both.
Step 1. The package: structure, signatures, approvals
treeship package verify retail.treeshipFrom the shopping demo's package, verbatim, with the twelve per-artifact inclusion lines folded:
PASS receipt.json -- Parses as valid Session Receipt
PASS type -- Correct receipt type
PASS determinism -- receipt.json round-trips identically (structural, NOT a signature)
WARN receipt_body_binding -- timeline/side-effects/narrative are NOT signed in this package — only the artifacts and Merkle root are cryptographically bound. ...
PASS merkle_root -- Merkle root matches recomputed value
PASS inclusion:art_a58a450ef62841c91d85fe27cca7d987 -- Inclusion proof valid
... (one line per artifact)
PASS leaf_count -- Leaf count matches artifact count
PASS timeline_order -- Timeline is correctly ordered
19 passed, 0 failed, 1 warnings
✓ package verifiedWhat each line establishes:
| Check | It proves | It does not prove |
|---|---|---|
receipt.json, type | The file is a well-formed session receipt of the expected type. | Anything about its truth. |
determinism | The receipt re-serializes to the same bytes, so a digest of it is meaningful. | It is explicitly not a signature check. |
receipt_body_binding (always WARN on an unpublished package) | Nothing. It is a reminder: the headline, the narrative, the timeline summaries and the side-effect lists are the producer's prose and are not under any signature. | Do not quote the narrative as evidence. Quote artifacts. |
merkle_root | The root written in the receipt is the root of the tree over exactly the artifact ids listed. Ids are content-addressed, so an id names one exact set of signed bytes. | That those bytes are ones you have seen. The digest column beside each id is informational and is not itself recomputed by this step; the id is what the tree binds. |
inclusion:<id> | That artifact's digest was a leaf of the tree when the session was sealed. Nothing was added or removed since. | What the artifact says. |
leaf_count, timeline_order | The count and the ordering are internally consistent. | Wall-clock time. The timestamps are the signer's own clock. |
Since 0.31.2, the same command continues past the tree into the envelopes. From a from-source build, on a session with four chained actions and one that was signed without --parent:
PASS signature:art_7f536ce61435c17afd16c3af6484d4eb -- Ed25519 signature by key_ba973a7083d45f8d verifies; id and digest re-derived from the signed bytes
... (one line per artifact)
PASS chain_linkage -- 4 chained artifacts each name the previous one as parent, inside the signature
WARN chain_completeness -- 1 sealed artifact(s) were signed during the session but never chained onto it (art_cd9bf1738355ae7d9ee10125cdf24359); signed and sealed, but their order relative to the chain is the signer's claim only
WARN signer_trust -- signature(s) verify for the key(s) the package names, but 1 of them are not pinned trust roots here: key_ba973a7083d45f8d. Pin what you have decided to trust: treeship trust add key_ba973a7083d45f8d ed25519:tyFBDCyMLbUV2NtlPaBaTiZvktwXxxOcI2j7ME9GSrw --kind cert_issuer| Check | It proves | It does not prove |
|---|---|---|
envelopes (only when it fails) | The package carries a signed envelope for its artifacts. A package built before 0.31.2 fails here, and the message says to verify from a bundle or the hub, or to read structure only with --structural. | Anything else. When the envelopes are present this row is silent and the per-artifact rows speak. |
signature:<id> | The envelope in artifacts/<id>.json carries a valid Ed25519 signature from one of the keys in keys.json, its content-addressed id re-derives from the signed bytes, and the digest column matches. Replace the id with an invented one, or edit one byte of the envelope, and this row fails. | Whose key that is. |
chain_linkage | Every chained artifact names the previous one as its parent, inside the signature, so the order the package presents is the order the signer committed to. Reorder two receipts and this fails. | The order of unchained artifacts. |
chain_completeness (WARN when it applies) | Names every sealed artifact that was signed during the session but never chained onto it: signed and verifiable on its own, but its place in the sequence is the signer's unsigned claim. | That the list is complete. An artifact signed into another workspace is not in this package at all. |
signer_trust | PASS when every signing key is a trust root you pinned (or, on the producer's own machine, one of its own keys). WARN otherwise, with the exact pin command for each key. --strict makes the warning a failure. | That the key belongs to the company or person named in the receipt. That is Step 2. |
The line to look for in a dispute is the inclusion line for the artifact in question. In the shopping demo that is art_a58a450e…, the result receipt of the add the provenance gate refused. Its presence in the tree means the refusal was part of the sealed record, not reconstructed after the fact.
When the session spent a human approval, the merchant demo's package adds six more lines:
PASS replay-package-local -- no duplicate approval use inside package (1 uses scanned)
PASS approval-use-record-digest -- 1 use record(s) recompute identically
PASS approval-use-nonce-binding -- 1 use record(s) bind to content-addressed grant signed nonces
PASS approval-use-action-binding -- 1 consuming action(s) bind cleanly to content-addressed envelope(s)
PASS approval-use-chain-continuity -- 1 record(s) form a single connected linked list from one genesis with no cycles or forks
PASS replay-local-journal -- local Approval Use Journal passed, use 1/1
24 passed, 0 failed, 1 warningsTogether these say: one grant was issued, it was spent exactly once, the action that spent it is the one the signed use record names, and the use records form one unbroken chain. treeship approval uses <grant-id> prints the human-readable version: who spent it, on what subject, when, with the digest of the nonce (never the nonce). --strict turns missing approval evidence from a warning into a failure. --format json gives the same result to a script.
package verify checks that the signatures are valid for the keys the package names. It cannot decide whether those keys are the producer's; that is why signer_trust stays a warning until you pin them. Step 2 is not optional. On a package built before 0.31.2 the command fails at envelopes, and the signatures can only be checked the Step 2 way.
Step 2. The signatures: pin the key, verify the chain
Every artifact is a DSSE envelope signed with the producer's Ed25519 key. Step 1 verified those signatures against the keys the package carries; what it could not do is decide which key you trust. Ask the producer for their public key through a channel you would accept in a dispute, and pin it. Ask for a bundle of the chain too when the package predates 0.31.2, or when you want the chain walked with the same checks the producer's own treeship verify runs: anchoring, actor proof, authority.
On the producer's side, this is what they run (shown so you know what to ask for):
treeship keys export # prints the public key and the exact pin command
treeship bundle create --artifacts <every id in the chain> --tag retail-demo
treeship bundle export <bundle-id> --out retail-chain.treeshipOn yours:
treeship trust add key_35c719307744c26e ed25519:jwVAw8_amVMB1TDNbJO3UHW5uyKn3KJ63ujtnMAgG7I --kind cert_issuer --yes
treeship bundle import retail-chain.treeship
treeship verify art_e8aad1f89cb61f4ed5f4bbfb3f80a1ce # the chain head, here the placed orderVerbatim from a second ship that had never seen the first:
✓ verified (13 artifacts . chain intact)
anchoring: UNWITNESSED — 2m of claimed work with no external anchor; the timeline rests on the signer's own clock
target: art_e8aad1f89cb61f4e
actor: agent://shopping
actor proof: asserted
action: commerce.order.placed
time: 2026-09-07T18:12:42ZRead every line, because three of them are limits:
verified … chain intactis the claim you came for. Every envelope from the placed order back to the session root carries a valid signature from the pinned key, every content-addressed id re-derives from its bytes, and every parent link resolves. Change one byte anywhere and this line fails. Importing the bundle before pinning the key fails too, withno valid signature from any trusted key; that is the trust root doing its job.anchoring: UNWITNESSEDmeans no third party recorded that these bytes existed by a given time. The timestamps are the signer's clock, signed by the signer. If the dispute turns on when, ask whether the session was anchored to a hub checkpoint or a transparency log, and runtreeship verify <id> --max-unwitnessed 1hto fail unless every stretch of work was witnessed within your bound.actor proof: assertedmeans the stringagent://shoppingis a claim made by whoever holds the key. The signature proves the key; it does not by itself bind the key to a company or a person. That binding comes from an Agent Certificate (--certificate <path-or-url>) or from the producer's hub attachment. Without one, "the agent" in this record is "the holder of keykey_35c7…".
One more flag is worth knowing. --require-authority fails unless the receipt carries a mandate that was checked and passed. On the shopping demo it fails with AUTHORITY NOT ESTABLISHED: this receipt carries no action/v2 mandate, and the message adds: absence of a claim is not a passing check. Commerce receipts as shipped prove what the agent did and what the gates decided; they do not carry a delegation credential. That is the seam Verifiable Intent is designed to fill.
treeship verify <id> --full draws the chain as a timeline. --format json returns every check by artifact id, outcome, passed, failed, chain_linkage_ok, authority_ok and authority_checked, for a script.
If the session was published, skip the bundle: treeship verify https://treeship.dev/receipt/<id> fetches the envelopes from the hub and runs the same checks on your machine. The hub stores bytes; it never issues the verdict.
Step 3. One receipt, read
A receipt's signed payload is small and plain. The producer can hand you any single receipt as three base64 strings:
treeship receipt export art_a58a450ef62841c91d85fe27cca7d987 --format jsonThat gives message_b64 (the DSSE pre-authentication encoding), signature_b64 and public_key_b64. Any Ed25519 library in any language confirms it with verify(public_key, message, signature), no Treeship code. Decode the message and this is the whole signed statement for the refused add, from the shopping demo:
{"type":"treeship/action/v1","timestamp":"2026-09-07T18:05:36Z","actor":"agent://shopping",
"action":"commerce.tool.add_to_cart.result","parentId":"art_5ffe80dc…",
"meta":{"elapsed_ms":0,"events":[],"gate":"provenance","intent_recorded":true,
"result_digest":"sha256:3ab4b78a…","role":"shopping","session_tag":"341b50bd9f40",
"status":"blocked","tool":"add_to_cart"}}"status":"blocked" and "gate":"provenance" are under the signature. The reference's gate said no, and the agent's own runtime signed that it said no. What the gate saw, the product id it refused, is in the intent receipt one link up (parentId), as a digest of the arguments.
receipt export runs on the machine whose keystore holds the signing key, so it is something the producer sends you, not something you run on an imported bundle. On your own ship, treeship verify <id> after Step 2 covers the same signature.
What a PASS does not prove
This is the section to keep open during a dispute.
- Not that the commerce was right. Treeship authenticates statements; it does not adjudicate. A signed
blockedproves the gate refused. It does not prove the gate was correct to refuse, and a signedokdoes not prove the cart was a good idea. The gate's logic is the reference's, and the reference's policy is the deployment's. - Not the contents, unless you are shown them. Receipts carry digests: of the arguments, of the result text, of the cart, of the payment URL, of the order reference. A digest proves that a specific thing was there and has not changed. To learn what it was, the merchant supplies the preimage (the cart lines, the result text), you hash it with SHA-256, and compare. A merchant who kept no preimages has receipts that prove tampering did not happen and cannot prove what the agent saw. The integration guide tells producers to keep them.
- Not who.
actor proof: asserted, as above. A key, not a name, until a certificate or hub attachment binds it. - Not when.
anchoring: UNWITNESSED, as above. A signed timestamp is a signed claim. - Not completeness. A chain proves what was recorded, never everything that happened. A tool call that was never receipted leaves no trace, except that the shipped wrapper counts drops and the demos print a warning when any receipt was not written. Ask the producer for that count. Ask whether recording was on for the whole session. Gaps in parent links show up as a failed walk; calls that were never attempted to be recorded do not.
- Not the story. Everything under
receipt_body_binding: headline, narrative, timeline summaries. Prose, unsigned. - Not that the approval gate held. In the merchant demo the replay attempt shows
errorin the default run andblocked:approvalunder--enforce. In both, the Approval Use Journal refused a second use of the grant before anything was signed, andreplay-local-journal … use 1/1says so. What differs is whether the wrapper also stopped the tool. Read the journal line, not the status word.
A checklist for the file
treeship package verifypasses, and the inclusion line for the artifact in dispute is present.- The producer's public key is pinned from a channel you trust, not from inside the package.
treeship verify <chain head>passes on your ship, afterbundle import, withchain intact.- You have read the
anchoringandactor prooflines and decided whether they are good enough for this dispute. - For the artifact in dispute, you have the preimage from the producer and its SHA-256 matches the digest in the signed payload.
- If an approval is involved,
treeship approval uses <grant>shows the use count you expect, andapproval-use-nonce-bindingpassed. - You are quoting artifacts, not the narrative.
If every box is ticked, what you can say is precise: these signed statements, by the holder of this key, existed in this order when the session was sealed, and the ones I was shown preimages for say exactly what the producer claims. That is a great deal more than a log, and it is not more than that.
Claude Commerce Agents
Wire tamper-proof receipts into anthropics/commerce-agents on the Messages API, the Agent SDK, and Managed Agents, verify a session offline, and know exactly what a receipt does and does not prove.
Payment Proofs
Creating verifiable payment receipts with Treeship artifacts and approval nonces.