TAMPER
SIGNAL
← Docs home

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.

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

The tamper-signal command

CommandWhat it does
tamper-signal initScaffold 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 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.
tamper-signal logRender archived run history as a per-metric trend across runs, one row per period. Read-only; exit 0.
tamper-signal doctorIntegration self-check with actionable fixes: Python version, key present and untracked, .gitignore coverage, chain verifies. Exit 0 = healthy.
tamper-signal exportWrite 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 serveServe receipts/ on localhost with CORS, for development.
tamper-signal assetsCopy 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 anchorRecord the chain in a Sigstore transparency log under your identity. Optional, see Power user.
tamper-signal annotateAttach 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 timelineWrite 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 custodyShow the CLI-local custody view — run cadence and archived prior chains, each re-verified — without publishing that history. Read-only.
tamper-signal watchPoll 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 reviewList, 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 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.
--as replace|periodingestreplace (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.
--bundleexportWrite a verified zip (data file + chain.json + receipts) for offline re-verification, instead of table.json.
--pub <path>verify, ingestPublic 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>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.

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
Formats: one difference

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.

CommandOn Node
tamper-signal doctoruse tamper-signal verify (exit 0 = healthy)
tamper-signal anchorPython only (Sigstore anchoring; Node support planned for 2.1)
tamper-signal custodyPython only
tamper-signal watchPython only (the chains it writes are read and verified by the Node stack)
tamper-signal reviewPython 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.