verify
Verify a receipt by URL, file path, or local artifact ID. Optionally cross-verify against an Agent Certificate.
treeship verify accepts three target shapes and one optional cross-check:
- a URL to a receipt JSON document served by the Hub
- a file path to a local
.treeshipor.agentpackage directory - an artifact ID from local storage (the original v0.1 path; chain walk + signature checks)
Pair any of those with --certificate to also confirm the session stayed inside the envelope of an Agent Certificate.
Usage
treeship verify <url-or-path-or-artifact-id> [OPTIONS]Options
| Option | Description |
|---|---|
--certificate <path-or-url> | Cross-verify against an Agent Certificate (.agent package or URL) |
--no-chain | Verify only this artifact, do not walk the parent chain. Artifact-ID form only. |
--max-depth <N> | Maximum chain depth to walk (default 20). Artifact-ID form only. |
--full | Show full chain timeline with box-drawn cards. Artifact-ID form only. |
--format json | Global flag. Machine-readable output for CI pipelines. |
--quiet | Global flag. Exit code only, no output. |
Exit codes
| Code | Meaning |
|---|---|
0 | Verified (pass on the local artifact path; structural-pass on URL/package targets) |
1 | Verification failed (signature, Merkle, inclusion proof, determinism, or empty receipt) |
2 | Cross-verification failed (cert mismatch, unauthorized tool call, expired cert) |
3 | Network or filesystem error (could not fetch URL or read file) |
Examples
treeship verify https://api.treeship.dev/v1/receipt/ssn_abc123 ✓ Downloaded receipt
✓ Merkle root verified
✓ 2/2 inclusion proofs passed
✓ Leaf count matches artifact count
✓ Timeline ordering verified
✓ Chain linkage intact
Structurally consistent.
⚠ signatures and issuer were NOT verified from this source
checked: Merkle root, inclusion proofs, leaf count, timeline order
not checked: who signed this receipt (no signature or trust-root check)
hint: to verify signatures against your trust roots, use the local
artifact form: treeship verify <artifact-id>
Session: ssn_abc123
Ship: ship_demo
Agent: researcher
Duration: 4m 22s
Actions: 28A fetched receipt earns structural-pass, never pass: this surface runs only keyless, self-referential checks and never opens the trust store, so it deliberately refuses to call the result "authentic". Signature verification against your trust roots requires the local artifact-ID form. An empty receipt (zero artifacts) is fail, not a vacuous pass.
The human-readable mirror at treeship.dev/receipt/... is also accepted; the CLI rewrites /receipt/ to /v1/receipt/ automatically.
treeship verify .treeship/sessions/ssn_abc123.treeshipLoads the on-disk package, runs the full bag of checks (determinism, Merkle root, inclusion proofs, leaf count, timeline ordering), and prints the same checkmark output as the URL form.
treeship verify ./researcher.agentLoads the certificate from researcher.agent/certificate.json, checks that the issuer key is pinned in your local trust store under kind agent_cert, verifies the Ed25519 signature, and prints the certificate metadata.
Trust pinning is mandatory and fail-closed: if the issuer key is not in your trust store, verification fails — with no trust roots configured for agent certificates when the store has nothing for kind agent_cert, or untrusted issuer when it has other keys. Pin an issuer first:
treeship trust add <key_id> <pubkey> --kind agent_cert
# or sync from your hub:
treeship hub sync-trusttreeship verify https://api.treeship.dev/v1/receipt/ssn_abc123 \
--certificate ./researcher.agentVerifies the receipt, then runs cross-verification:
✓ Certificate verified
✓ Ship IDs match
✓ All 12 tool calls authorized by certificate
Complete trust loop verified.Fails with exit code 2 if the certificate's issuer key is not pinned in your trust store (kind agent_cert), the receipt's ship_id does not match the certificate's identity.ship_id, the certificate is expired or not yet valid at the verify time, or any tool the session called is missing from the certificate's capabilities.tools.
treeship verify art_f7e6d5c4 --fullThe original local-artifact path. Walks the parent chain, verifies every Ed25519 signature, checks approval nonce binding, and prints a box-drawn timeline.
treeship verify $RECEIPT_URL --format json --certificate $CERT_PATH \
| jq -e '.cross_verify.ok == true'For URL and package targets the JSON object looks like:
{
"outcome": "structural-pass",
"signatures_verified": false,
"issuer_verified": false,
"note": "structural checks only (Merkle root, inclusion proofs, leaf count, timeline). Signatures and issuer are NOT verified from a fetched receipt; use the local artifact-ID verify path for signature verification.",
"receipt": { "session_id": "ssn_abc123", "ship_id": "ship_demo", "schema_version": "1", "artifact_count": 28 },
"checks": [ { "name": "merkle_root", "status": "pass", "detail": "..." } ]
}outcome on this surface is "structural-pass" or "fail" — never "pass" — and signatures_verified / issuer_verified are always false. Do not gate CI on .outcome == "pass" for a URL or package target; it will never match. Gate on .outcome == "structural-pass" (or just the exit code), and on .cross_verify.ok == true when --certificate is used. The cross_verify block carries ship_id_status, certificate_status, the three tool-call lists, and the roll-up ok boolean.
The local artifact-ID form emits "outcome": "pass" | "fail" with per-check results — that is the surface where real signature verification happens.
What the verifier checks
The exact set of checks depends on what's available at the target:
- URL mode runs every check that's derivable from the receipt JSON alone: Merkle root recomputation, inclusion proofs, leaf count, timeline ordering, chain linkage. Per-artifact Ed25519 signature checks need the original envelope bytes, which only the local-storage and
.treeshippackage paths have. .treeshippackage mode runs everything URL mode runs plus determinism (the on-diskreceipt.jsonround-trips byte-identical) and any signature checks the package preserves..agentpackage mode verifies the certificate's Ed25519 signature and requires the issuer key to be pinned in your trust store (kindagent_cert). A certificate is never accepted on its embedded key alone — that would make every certificate self-signed. Fails closed when no trust roots are configured.- Artifact-ID mode is the original local-storage path: walks the parent chain, verifies every signature, enforces nonce binding on approvals, checks expiry. See the chain-verification guide for full semantics.
The cross-verification path additionally verifies that the receipt's session.ship_id matches the certificate's identity.ship_id, the certificate was valid at verify time (issued_at <= now <= valid_until), and every tool the session called is present in the certificate's capabilities.tools list. See the cross-verification concept for the full semantics.
Pre-v0.9.0 receipts (before schema_version and session.ship_id were added) verify cleanly under the URL and package paths but cannot complete cross-verification because the receipt has no ship_id. The CLI reports Receipt has no ship_id (legacy receipt; cannot cross-verify) and exits 2.
Approval Authority replay rows (v0.9.10)
When the package contains evidence of consumed ApprovalUse records, verify emits one row per replay level — each row pinned to a specific invariant. The strongest level the package's evidence supports wins; nothing silently downgrades.
| Row | What it proves | Available when |
|---|---|---|
replay-package-local | No duplicate uses inside this package | Always |
replay-local-journal | The workspace's local journal has not exceeded max_uses for the recorded (grant_id, nonce_digest) | Verifier has Treeship workspace (treeship package verify from a workspace) |
replay-included-checkpoint | An embedded JournalCheckpoint's record_digest recomputes — record range wasn't tampered after sealing | Package carries one or more checkpoints |
replay-hub-org | A signed Hub checkpoint validates global single-use across machines | Package carries a kind: hub-org checkpoint that signature-verifies AND covers every embedded use_id. The Hub server itself is out of scope for v0.9.9-v0.9.10. |
Plus four bundle-level integrity rows added in v0.9.10:
| Row | What it proves |
|---|---|
approval-use-record-digest | Every embedded use record's record_digest recomputes from canonical form |
approval-use-nonce-binding | Each use's nonce_digest equals sha256(grant.nonce) for a content-addressed grant in the package |
approval-use-action-binding | Each consuming action's meta.approval_use_id resolves to a use in the package, and the action's approval_nonce hashes to that use's nonce_digest. The action envelope is content-addressed against its filename — substituted/forged envelopes fail this row. |
approval-use-chain-continuity | Embedded uses + checkpoints form a single connected linked list from one genesis: no forks, cycles, or disconnected subchains. |
The honesty rule
A row reports ✓ only when the matching evidence is present and verified. ✗ only when the evidence is present and a check failed. - (or absent) when the evidence isn't in the package at all — never silent pass. Treeship will physically refuse to print "global single-use enforced" or "replay-hub-org passed" without a real Hub-signed checkpoint that verifies cleanly AND covers every use_id.
Strict mode
treeship package verify --strict <pkg>Promotes every approval-evidence warning to a hard failure. replay-*, approval-use-record-digest, approval-use-nonce-binding, approval-use-action-binding, and approval-use-chain-continuity all participate. Existing receipt-determinism / event-log warnings stay warnings.
Useful in CI:
treeship package verify --strict ./session.treeship
echo "exit code: $?" # non-zero on any approval anomalyFor the full ladder and the v0.9.10 bypass paths the binding rows close, see Replay levels and Approval Authority.