receipt
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:
- 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). - 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. - Snapshots again and diffs. A delete plus a create with identical content
is one
renamedpair; identical content with changed permission bits ismode_changed. - Checks the diff against the declared scope (exact paths or globs like
app/*.py, matched with/on every OS). - Writes command, task, diff, scope and verdict to
receipts/<timestamp>-<random>.jsonwith 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 byreceipt 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)
| File | Size | Uploaded | |
|---|---|---|---|
| receipt_evidence-0.2.0.tar.gz | 42.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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