Skip to main content

receipt

ci PyPI

Verified receipts for what actually happened -- not what was claimed.

A shell command, an AI agent's tool call, a click in a browser: each one comes with a self-report ("I edited auth.py", "Saved!"). receipt checks the self-report against reality, independently, and writes down what it found in a tamper-evident file you can hand to someone else.

what happened how you capture it what gets checked
a shell command receipt run --declare ... -- cmd every file it touched, against the scope you declared
an AI agent's Edit/Write/Bash call the custody-hook Claude Code hook the bytes on disk are exactly what the tool says it wrote
one browser interaction with capture(page): ... (Playwright) every network request it really made, and optionally the files it caused

Every receipt is JSON with a sha256 over its content -- the Providence shape -- and providence check tells you if one was edited after it was written.

pip install receipt-evidence     # zero dependencies; installs `receipt`, `custody-hook`, `providence`

One package, stdlib only. (It used to be four: receipt-evidence, custody-evidence, clicked-evidence and providence-evidence. The old names still install and point here.) Import it as receipt_evidence.

Three statuses throughout, same as invariant, firedrill and carabiner: pass, fail, and unverified -- a check that couldn't decide is never reported as a pass.


1. Shell commands: receipt run

$ receipt run --task "fix the auth bug" --declare app/auth.py \
    -- python fix_auth.py
[FAIL] touched 1 undeclared file(s): app/payments.py
receipt written to receipts/20260908T121251Z-4f2c9a1b.json
receipt run --task "what this is supposed to do" \
  --declare path/one.py,app/*.py \
  --dir . --out receipts/ \
  -- your-command --with --args

pass -- touched only what was declared. fail -- touched something outside the declared scope, named exactly. unverified -- no --declare was given: plain audit logging, a normal use, so it does not fail the build; only a broken declared promise does.

What it does:

  1. Hashes and permission-bits every regular file under --dir (skipping .git, __pycache__, node_modules, .venv). A FIFO, socket or device node is skipped rather than opened -- open() on a FIFO with no writer blocks forever (a real bug: it hung two CI runs for 6 hours each).
  2. Runs the command, capturing stdout/stderr/exit code/timing, and redacts secret-shaped text before storing any of it. A command that never launches (bad --dir, missing binary) still produces a receipt -- fail, with the launch error -- not a traceback and no evidence.
  3. Snapshots again and diffs. A delete plus a create with identical content is one renamed pair; identical content with changed permission bits is mode_changed.
  4. Checks the diff against the declared scope (exact paths or globs like app/*.py, matched with / on every OS).
  5. Writes command, task, diff, scope and verdict to receipts/<timestamp>-<random>.json with a sha256 of the record. Shape: schema/receipt.schema.json.

What touched means: added, removed, modified, renamed (either end), or permissions changed. A rename or chmod outside the scope is a real fail, named clearly -- sneaky.txt (renamed from output.txt).

What pass doesn't mean: "touched nothing outside the declared scope within --dir." A write to /tmp, ~ or a sibling directory is invisible here. Point --dir at the smallest tree that bounds the task.

2. AI agent tool calls: the Claude Code hook

custody-hook runs on every Edit, Write and Bash call a Claude Code session makes, and writes one receipt per call to .custody/receipts/.

For Edit/Write it rebuilds the text the tool reports having left on disk -- Write's content, or Edit's originalFile with its oldString -> newString replacement -- and compares it to the file's real bytes:

what's on disk status
exactly what the tool reported writing pass
anything else (a partial write, a formatter or another process racing it) fail
the file is gone after the call unverified
not UTF-8, so not comparable as text falls back to a before/after sha256

A file's CRLF line endings and a UTF-8 BOM are not differences: Edit keeps both on disk while reporting normalized text (measured against real Claude Code, not assumed). Bash calls are recorded -- every file the command changed -- but judged unverified: an agent's shell command declares no scope to check against.

Wire it into .claude/settings.json (a project) or ~/.claude/settings.json (everywhere):

{
  "hooks": {
    "PreToolUse": [{ "matcher": "Edit|Write|Bash",
      "hooks": [{ "type": "command", "command": "custody-hook" }] }],
    "PostToolUse": [{ "matcher": "Edit|Write|Bash",
      "hooks": [{ "type": "command", "command": "custody-hook" }] }]
  }
}

custody-hook must be on the PATH Claude Code runs hooks with -- a project-local .venv isn't, and a hook command that doesn't resolve fails silently. Use pipx install receipt-evidence, or put the absolute path in command. Add .custody/ to .gitignore.

The hook never blocks a tool call and always exits 0: it's a witness, not a gate. Verified end to end in a real Claude Code session.

3. Browser interactions: capture()

from receipt_evidence.browser import capture

with capture(page, task="delete account button", watch_dir="./data") as c:
    page.click("#delete-account-button")
    page.wait_for_load_state("networkidle")

c.write("receipt.json")        # a Providence bundle
print(c.result["requests"])    # every request/response actually made
print(c.result["changes"])     # what landed in ./data because of it

c.result["requests"] is every request made during the block -- URL, method, POST body, status, or a failure reason if it never got a response. That's what the browser actually sent, not what the page's JavaScript claims. watch_dir (optional) snapshot-diffs a local directory too, closing the loop from a click to a real backend side effect.

Works with any Playwright Page -- Playwright is never imported, only .on()/.remove_listener() are called -- so there's no dependency on it or on any particular version.

4. The bundle format: providence

$ providence check proof/
PASS  proof/ is a conformant Providence bundle

$ providence check proof/   # after someone hand-edits a file in it
FAIL  proof/manifest.json: items[0] (id=no_negative_payments): sha256 mismatch -- ...

One versioned shape for "content + sha256(content) + when", in two forms: a single JSON file (payload + sha256 of its canonical serialization), or a directory (manifest.json + one <id>.json per item). Full spec: PROVIDENCE_SPEC.md. providence check verifies every hash, that every file is accounted for, and rejects item ids or symlinks that would reach outside the bundle.

providence convert-invariant proof/ out/     # an invariant --evidence dir
providence convert-receipt receipts/x.json out/

What this is not: a signature. A sha256 proves content wasn't edited after the bundle was written -- not who wrote it, nor that the whole bundle wasn't regenerated from scratch.


Redaction, and what it doesn't mean

Captured stdout/stderr, argv, URLs and POST bodies are swept for secret-shaped text (receipt_evidence/redact.py: KEY=value pairs, credentialed URLs, well-known token prefixes, PEM private keys) before anything is written. It's a regex net for common shapes, not a guarantee: a secret with no recognizable shape gets through. Read a receipt before sharing it if a command might have printed something sensitive.

Python API

from receipt_evidence.core import run                        # receipt run, as a function
from receipt_evidence.browser import capture                 # browser interactions
from receipt_evidence.providence.check import check_bundle   # [] means conformant
from receipt_evidence.providence.spec import make_bundle     # write your own

Tests

pip install -e . jsonschema
python tests/test_receipt.py
python tests/test_providence.py
python tests/test_claude_core.py
python tests/test_claude_hook.py     # tool_response shapes captured from real Claude Code
python tests/test_browser.py

pip install playwright && python -m playwright install chromium
python tests/test_browser_live.py    # a real Chromium, a real server

CI runs on Linux and Windows, plus the live-browser suite and a cold install of the built wheel that exercises all three commands.

Not here yet

  • Writes outside the watched directory (/tmp, ~) aren't seen by receipt run -- that needs OS-level tracing, a separate project.
  • No network capture for shell commands, only for browser interactions.
  • Policy beyond a flat declared scope belongs in invariant, which reads these receipts as one of its check types.

MIT licensed.

Release files for receipt-evidence 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for receipt-evidence 0.2.0
File Size Uploaded
receipt_evidence-0.2.0.tar.gz 42.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for receipt-evidence 0.2.0
File Interpreter ABI Platform
receipt_evidence-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 72.2 kB

Release files / receipt_evidence-0.2.0.tar.gz

Download URL receipt_evidence-0.2.0.tar.gz
Size 42.8 kB
Tags Source
SHA-256 checksum
How to use checksums
c4853e6bbb9b1d011bcd65b270dc78e38c36f3a552ca9ea05632ea3aaf9baec4
BLAKE2b-256 checksum
How to use checksums
f5f317ba95031f739a5f3d738fa661d55c544e4ae0c4b276672d053ad20d0aa9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release files / receipt_evidence-0.2.0-py3-none-any.whl

Download URL receipt_evidence-0.2.0-py3-none-any.whl
Size 29.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bfd397c3a8002f4df205b82bab196d2ac98d2de4d8aeb206503f125d54c14752
BLAKE2b-256 checksum
How to use checksums
fb6d42891eb9d342e94a503e437e29f7e4a9a7b5793932a13d452c2a49e47bae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page