Reference
CLI
Two command-line tools, one set of ideas. receipts ships with the Python package; tamper-signal ships with the npm package. The chains they produce are interchangeable, and they share the same exit codes.
The exit code is the light
Every verifying command returns the traffic light as its process exit code, so a shell or CI job can branch on it without parsing anything.
| Exit | Light | Meaning |
|---|---|---|
0 | green | Every link verifies, every signature checks. |
2 | yellow | Verifies, with caveats a human should look at. |
1 | red | Chain broken at a specific link. |
Python: receipts
| Command | What it does |
|---|---|
receipts init | Scaffold keys, a receipts/ directory, and .gitignore safety. Idempotent. |
receipts ingest <file> | Pin a source file: record evidence and semantic hashes plus control totals, sign the source receipt. Writes 000_source.json and chain.json. |
receipts verify <chain> | Walk the chain and emit the light. Add --data to also check a current file against the final receipt. |
receipts diff | Compare two runs: per-stage code-hash changes and a structured totals delta, including which period buckets moved. Read-only; exit 0 with or without differences. |
receipts log | Render archived run history as a per-metric trend across runs, one row per period. Read-only; exit 0. |
receipts doctor | Integration self-check with actionable fixes: Python version, key present and untracked, .gitignore coverage, chain verifies. Exit 0 = healthy. |
receipts export | Write receipts/table.json, the canonical attested table for the data tab. Refuses if the data does not match the final receipt. |
receipts serve | Serve receipts/ on localhost with CORS, for development. |
receipts anchor | Record the chain in a Sigstore transparency log under your identity. Optional, see Power user. |
receipts demo | Run the whole story end to end on sample data, then serve a badge showing green, yellow, and red. |
Common flags
| Flag | On | Effect |
|---|---|---|
--origin "<text>" | ingest | Free-text label for the source, shown to humans in the signal UI. |
--key <path> | ingest | Private signing key. Overridden by the TAMPER_SIGNAL_KEY environment variable when set. |
--out <dir> | ingest | Receipts directory to write into. |
--band <pct> | ingest | Tolerance band for cross-run drift, e.g. 5% or 0.05. Signed into the manifest; defaults the settling window to 72h. |
--settle <hours> | ingest | Settling window, e.g. 72h or 3d. Recent buckets may drift within the band; settled buckets may not. Defaults the band to 0.05. |
--bucket-column <name> | ingest | Date column to key per-period buckets off. Omit it and a single date-shaped column is detected automatically. |
--pub <path> | verify | Public key to trust. Repeats: any listed key verifies, which is how key rotation works. |
--data <file> | verify | Check the file the dashboard reads against the final receipt. |
--warn-drift | verify | Flag any control-total movement across links as a yellow caveat. Off by default, because filters and aggregations legitimately move totals. |
--json | verify | Emit the structured verdict (schema below) instead of prose. |
--anchor | verify | Require and check a transparency-log anchor in addition to signatures. |
JavaScript: tamper-signal
The Node CLI mirrors the Python one with the same exit codes.
tamper-signal keygen --out keys/
tamper-signal ingest export.csv --origin "TikTok export, May 2026" --out receipts/
tamper-signal verify receipts/chain.json --pub keys/signing.pub --data current.csv
tamper-signal export receipts/chain.json --data current.csv # writes receipts/table.json
The Python CLI reads .xlsx and .xlsm as well as .csv, .tsv, .json, and .ndjson. The Node CLI reads everything except the Excel formats. Because the semantic hash is identical across formats, a common pattern is to ingest an xlsx once with Python and then verify against a CSV in Node; the chain interoperates.
What lives in which stack
| Python receipts | JavaScript tamper-signal |
|---|---|
receipts init | tamper-signal keygen (the receipts/ dir is created on first ingest) |
receipts ingest | tamper-signal ingest |
receipts verify | tamper-signal verify |
receipts diff | tamper-signal diff |
receipts log | tamper-signal log |
receipts export | tamper-signal export |
receipts serve | your bundler's static server, or tamper-signal/express |
receipts doctor | tamper-signal verify (exit 0 = healthy) |
receipts anchor | Python only |
The JSON verdict
Both CLIs emit the same structured verdict with --json. This is what CI jobs and coding agents parse.
{
"verdict": "green", // green | yellow | red
"exit_code": 0,
"spec_version": "1.2",
"receipts": 3,
"transforms": 2,
"stages": ["source", "clean", "aggregate"],
"final_row_count": 304,
"caveats": [],
"caveat_details": [], // typed period-over-period findings; [] when none
"broken_link": { // null when green
"link": [1, 2],
"stage": "aggregate",
"expected_input_hash": "a3f1…9c",
"found_input_hash": "77b2…d4",
"totals_delta": ["row_count 4987 -> 304 (-4683)"]
},
"data_mismatch": null,
"receipt_mismatch": null,
"report": ["human-legible lines"],
"anchor": [] // only when --anchor passed
}
When a tolerance is declared at ingest, a recurring refresh can earn extra yellow caveats, each with a typed entry in caveat_details: band breach (a recent bucket drifted beyond the declared band), settled movement (a bucket older than the settling window changed at all), bucket removed (an interior period present last run is gone this run), and bucket loss (the bucket column is no longer detected, so period judgment is unavailable). Each entry names the metric, the bucket count, and the worst bucket with before, after, and delta. These are still yellow, never red.