TAMPER
SIGNAL
← Docs home

Quickstart

Your first verified chain

We will take a source file from raw export to a green light, then deliberately tamper with it and watch the light turn red and name the broken link. About five minutes. Python 3.11 or newer.

The one-command tour

In a hurry? pip install tamper-signal then receipts demo runs this entire story end to end on sample data: ingest, two transforms, verify (pass), tamper, verify (fail), and serves a badge so you can see green, yellow, and red side by side. The walkthrough below is the same thing, by hand, so you understand each step.

1. Install

Tamper Signal ships for both stacks, and the chains are interchangeable. Pick whichever your pipeline already speaks.

# Python 3.11+ : gives you the `receipts` CLI and the tamper_signal package
pip install tamper-signal

# or Node 18.17+ : gives you the `tamper-signal` CLI and a programmatic API
npm install tamper-signal

Confirm it is on your path:

receipts --help        # exits 0 when healthy

2. Scaffold keys and folders

One idempotent command generates your signing keypair, writes a receipts directory, and adds the private key to .gitignore so it can never be committed by accident.

receipts init

You now have keys/signing.key (private, PEM, never commit) and keys/signing.pub (public, hex, safe to commit), plus an empty receipts/.

Guard the private key

keys/signing.key is the signet ring from the concepts page. Anyone holding it can sign receipts as you. init adds keys/ and *.key to .gitignore; leave that in place. In CI, pass the key through the TAMPER_SIGNAL_KEY environment variable instead so no file ever touches disk.

3. Ingest the source

Ingest pins the original export in place: it records the evidence hash of the raw bytes, the semantic hash of the data, and the control totals, then signs a source receipt. The --origin text is free-form, for humans, and shows up later in the signal.

receipts ingest sample_export.xlsx \
  --origin "TikTok export, May 2026" \
  --key keys/signing.key \
  --out receipts/

This writes receipts/000_source.json and receipts/chain.json. Accepted formats: .xlsx, .csv, .tsv, .json (array of objects), and .ndjson. The semantic hash is identical across all of them, so an xlsx ingest will later verify against a CSV copy of the same rows.

4. Wrap a transform

Any function that takes a list of records and returns a list of records (or a pandas DataFrame) becomes a signed stage by adding one decorator. The wrapper verifies the chain tail first, refuses to run if its input does not match, runs your code, then signs and appends a receipt.

from tamper_signal import receipt_step

@receipt_step(chain_dir="receipts/", key_path="keys/signing.key")
def transform_clean(records):
    # drop rows with no campaign name
    return [r for r in records if r.get("campaign_name")]

The JavaScript equivalent is receiptStep, an async wrapper with the same contract. Both are covered in the API reference.

5. Verify: the green light

Walk the chain, check every signature and every link, and reduce it to a light. Add --data to also check that the file your dashboard actually reads still matches the final receipt.

receipts verify receipts/chain.json \
  --pub keys/signing.pub \
  --data dashboard.xlsx
✓ The light is green, the data is clean.
  3 receipts · 2 transforms · source → clean → aggregate
  final row_count 304

The exit code is the light: 0 green, 2 yellow (verifies, with caveats), 1 red. That makes it trivial to gate CI, covered in Power user.

6. Tamper, and watch it turn red

Now break it on purpose. Edit a single value in the data the dashboard reads, drop a few rows, anything, then verify again.

receipts verify receipts/chain.json --pub keys/signing.pub --data dashboard_edited.xlsx
✗ CHAIN BROKEN at link 1 → 2 (transform_aggregate)
  expected input hash  a3f1…9c   (output of transform_clean)
  found    input hash  77b2…d4
  totals delta vs upstream: row_count 48212 → 48190 (-22), spend_(usd) -98.40

This is the whole promise in one screen. The light is red, it names the exact stage where continuity broke, and the control totals tell you twenty-two rows and $98.40 went missing right there.

What you just proved

While the chain was green, every number on the dashboard descended from that original May export through known code, unchanged. The moment one value moved, the light found the exact link and quantified the drift. You did not re-check 48,000 rows by hand; the receipts did it for you.

Where to go next