TAMPER
SIGNAL
← Docs home

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.

ExitLightMeaning
0 greenEvery link verifies, every signature checks.
2 yellowVerifies, with caveats a human should look at.
1 redChain broken at a specific link.

Python: receipts

CommandWhat it does
receipts initScaffold 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 diffCompare 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 logRender archived run history as a per-metric trend across runs, one row per period. Read-only; exit 0.
receipts doctorIntegration self-check with actionable fixes: Python version, key present and untracked, .gitignore coverage, chain verifies. Exit 0 = healthy.
receipts exportWrite receipts/table.json, the canonical attested table for the data tab. Refuses if the data does not match the final receipt.
receipts serveServe receipts/ on localhost with CORS, for development.
receipts anchorRecord the chain in a Sigstore transparency log under your identity. Optional, see Power user.
receipts demoRun the whole story end to end on sample data, then serve a badge showing green, yellow, and red.

Common flags

FlagOnEffect
--origin "<text>"ingestFree-text label for the source, shown to humans in the signal UI.
--key <path>ingestPrivate signing key. Overridden by the TAMPER_SIGNAL_KEY environment variable when set.
--out <dir>ingestReceipts directory to write into.
--band <pct>ingestTolerance band for cross-run drift, e.g. 5% or 0.05. Signed into the manifest; defaults the settling window to 72h.
--settle <hours>ingestSettling 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>ingestDate column to key per-period buckets off. Omit it and a single date-shaped column is detected automatically.
--pub <path>verifyPublic key to trust. Repeats: any listed key verifies, which is how key rotation works.
--data <file>verifyCheck the file the dashboard reads against the final receipt.
--warn-driftverifyFlag any control-total movement across links as a yellow caveat. Off by default, because filters and aggregations legitimately move totals.
--jsonverifyEmit the structured verdict (schema below) instead of prose.
--anchorverifyRequire 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
Formats: one difference

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 receiptsJavaScript tamper-signal
receipts inittamper-signal keygen (the receipts/ dir is created on first ingest)
receipts ingesttamper-signal ingest
receipts verifytamper-signal verify
receipts difftamper-signal diff
receipts logtamper-signal log
receipts exporttamper-signal export
receipts serveyour bundler's static server, or tamper-signal/express
receipts doctortamper-signal verify (exit 0 = healthy)
receipts anchorPython 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
}
Period-over-period caveats

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.