Skip to main content
You have a pack and you want to check it yourself. You need Node and nothing else. You do not need a Sanning account, an API key, or any cooperation from the party who gave you the pack.

The two steps

A pack is a zip. The kernel reads the bundle inside it, so extract first.
A verified pack prints its per-record results and exits 0:
Leave out --logs when the pack has no logs/ directory. A pack of hand-off records has none, and --logs naming a directory that does not exist exits 2. From 0.17.0, a --logs folder is read by one rule. The command reads each regular file named <event_id>.json as that record’s disclosed content. It never follows a link, never enters a subfolder, and names every other entry in a note: line without reading it. It exits 2 for a name that is not valid UTF-8, for more than 50,000 entries, and for more than 1 GiB of <event_id>.json files. Both @sanning/proof and sanning-proof apply the same rule, so one folder no longer reads differently in the two.

Exit codes

This is the one table every verb of @sanning/proof and sanning-proof uses, verify, export and case alike: A pack whose declaration says a record’s content is withheld on purpose still reads verified: a hand-off record commits to a message’s hash and the pack carries no file for it, and the declaration says so, so its content binding is undetermined rather than missing. A pack with no declaration (built with no signing seed) and a record that discloses nothing reads incomplete instead, because nothing says the absence was on purpose. Either way the signatures, the inclusion proofs and the declaration, where the pack has one, are still checked, and the output says so on its own lines. This command reads one pack. To read several parties’ packs as one case, see Hand over and verify a case.
From 0.14.0, --files matches a file beside the pack to the record that commits to it. Each offered file is hashed and checked against event.content_hash, or a file part’s content_hash inside a hand-off message. A file is never matched by name, and case takes the same flag. See Command line.
From 0.17.0, --files accepts a folder. The folder stands for the regular files inside it, found recursively with hidden files included, and each is matched by SHA-256 as a named file is. Links are never followed. A pipe is not read, and a file that cannot be read is attempted and then named as skipped (unreadable); the report names each in a skipped list with the reason. A folder nested more than 32 levels deep, holding more than 50,000 names, or holding a name that is not valid UTF-8 exits 2. Files found inside folders share 1 GiB per run, and one that would pass it is skipped as too_large. A file you name is read at any size. You can mix files and folders, and the verdict and exit code never move.
From 0.17.0, a reading fails closed on a tree size it cannot trust. An anchor-trace event whose inclusion.leaf_count differs from the leaf_count its signed checkpoint carries reads failed, and so does a checkpoint record with no integer leaf_count. A stamp inclusion whose stated count is a boolean reads failed in both kernels. Integers on the anchor-trace, stamp, and agent-proof paths are read by value, so 3.0 is 3 and an honest bundle written with 3.0 verifies.
From 0.14.0, parsing is stricter. A repeated key anywhere in the JSON reads malformed, exit 2. event_id, payload_hash, payload_ref, subject, and every time field a verdict carries, are checked against their exact wire shapes rather than accepted as any string. Nothing on the public record is affected.

Two errors you might hit first

Both exit 2, before any check runs. You pointed it at the zip. The kernel reads JSON, not an archive:
Extract the zip and point at bundle.json inside it. You pointed it at the directory.
Name bundle.json, not the folder that holds it. Only --logs and --files take a folder.

Check the tamper-evidence yourself

Do not take the verdict on trust. Break the pack and watch it fail.
It exits 1 and names the record you touched. Replace SOME_EVENT_ID with any filename from logs/.
Confirm the file actually changed, with sha256sum before and after. An edit that writes the same byte back produces an identical file, which verifies clean and looks exactly like a passing tamper test.

Confirm the witness on the public record

The verdict above is offline. It names a witness without contacting it. To check that the witness holds those bytes, pass a gateway:
That turns a claim you read into one you checked. See What a verdict means for why the two are different. Any Arweave gateway works, and passing more than one is a comma-separated list. Nothing binds you to the one we name: that independence is the point, and checking through a gateway you chose is a stronger result than checking through ours.
Only an arweave witness with a 43-character transaction id is re-fetched. That is the one witness kind and id shape the kernel knows how to look up. A witness of any other kind, or an arweave witness whose id is not exactly 43 characters, is reported as not checked rather than skipped silently, and the run answers partial and exits 3.
From 0.16.0, a pack can carry witness-cache.json beside bundle.json. It names a witnessed time the pack’s own builder already looked up. This offline check prints it labelled not checked; pass --gateways to check it yourself.
A stamp reaches the chain before an index can serve it. It is submitted, then bundled, then indexed, so a gateway can answer 404 for a few minutes on a stamp that exists. That is exit code 3 as well, and it is a statement about reachability rather than about your evidence. The offline check does not wait on any of it. Retry the online one.

Python

sanning-proof on PyPI is the same kernel and ships the same command, so a Python shop needs no Node to check a pack:
The verbs, the flags and the exit codes match npx @sanning/proof exactly. Both kernels are graded against one corpus, and a disagreement between them on the same input is release-blocking. Use it as a library instead when you are checking a pack inside your own code:
Two kernels, one corpus. They agree record by record, and disagreeing is a bug we treat as release-blocking.