Reference
CLI
One command, two stacks. As of 2.0 the CLI is tamper-signal whether it ships with the Python package or the npm package (on Python, receipts still works as a deprecated alias). The chains are interchangeable and the exit codes are identical.
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. |
The tamper-signal command
| Command | What it does |
|---|---|
tamper-signal init | Scaffold keys, a receipts/ directory, and .gitignore safety. Idempotent. |
tamper-signal 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. --as replace (default) re-signs a fresh chain and archives the prior one; --as period brings a file back as the next period of the chain's run history, under a trusted signer. |
tamper-signal verify <chain> | Walk the chain and emit the light. Add --data to also check a current file against the final receipt. |
tamper-signal 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. |
tamper-signal log | Render archived run history as a per-metric trend across runs, one row per period. Read-only; exit 0. |
tamper-signal doctor | Integration self-check with actionable fixes: Python version, key present and untracked, .gitignore coverage, chain verifies. Exit 0 = healthy. |
tamper-signal export | Write receipts/table.json, the canonical attested table for the data tab. Refuses if the data does not match the final receipt. Add --bundle to write a verified zip instead: the data file plus chain.json and its receipts, which a recipient re-verifies offline with tamper-signal verify chain.json. |
tamper-signal serve | Serve receipts/ on localhost with CORS, for development. |
tamper-signal assets | Copy the bundled browser assets (light.js, badge.js, element.js, table.js, console.js) into a project, default --out badge/, so you can vendor the surfaces without digging through site-packages. |
tamper-signal anchor | Record the chain in a Sigstore transparency log under your identity. Optional, see Power user. |
tamper-signal annotate | Attach a signed reason (and optional self-declared author) to a receipt by its content hash, so the note is tamper-evident and cannot be silently retargeted. A correction supersedes a prior note by hash; nothing is overwritten. |
tamper-signal timeline | Write the narrow, published timeline.json (imports, changes, their top-level totals, signed annotations, and any pending changes) that the console renders as the chain of custody. Chain-tail-bound, and signed when a key is available. |
tamper-signal custody | Show the CLI-local custody view — run cadence and archived prior chains, each re-verified — without publishing that history. Read-only. |
tamper-signal watch | Poll a live HTTP/JSON-API or RSS/Atom feed and keep it on the same signed chain: new data auto-appends, a change to an already-settled period is withheld for review. Needs pip install "tamper-signal[watch]". --daemon/--interval loop; default is one stateless tick. See Power user. |
tamper-signal review | List, accept, or reject withheld watch changes. Each accept <hash> --reason signs its own human reason and commits the exact reviewed candidate; reject discards it without touching the chain. |
tamper-signal 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. |
--as replace|period | ingest | replace (default) re-signs a fresh chain and archives the prior one; period appends the file as the next run in history, judged against the prior run's signed band. period requires a trusted signer. |
--bundle | export | Write a verified zip (data file + chain.json + receipts) for offline re-verification, instead of table.json. |
--pub <path> | verify, ingest | Public key to trust. Repeats: any listed key verifies, which is how key rotation works. On ingest --as period, names a trusted signer other than the chain's own key. |
--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. |
One command, both stacks
As of 2.0 the command is tamper-signal on both the Python and the Node install, with the same subcommands and the same exit codes. (On Python, receipts still works as a deprecated alias and prints a notice; it is scheduled for removal in 3.0.)
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 install reads .xlsx and .xlsm as well as .csv, .tsv, .json, and .ndjson. The Node install 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.
Python-only subcommands
Every subcommand runs on both stacks except these, which are Python-only for now. The chains they produce are ordinary signed receipts that the Node install reads and verifies unchanged.
| Command | On Node |
|---|---|
tamper-signal doctor | use tamper-signal verify (exit 0 = healthy) |
tamper-signal anchor | Python only (Sigstore anchoring; Node support planned for 2.1) |
tamper-signal custody | Python only |
tamper-signal watch | Python only (the chains it writes are read and verified by the Node stack) |
tamper-signal review | 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.