git receipts — read the session as a story
recap answers "where are things". audit answers "where should I look".
Recap is the state-face you glance at; /gitreceipts:audit is the
attention-ladder you climb.
git receipts reconciles a Claude Code session log against the repo's git
history, then reads it back: each prompt you typed → the work it drove →
the commit it became. The checking always runs (git is the record the
agent can't rewrite); recap just doesn't lead with the verdict. Same
receipt, same checking — only the framing differs.
Prerequisite: the binary
command -v git-receipts >/dev/null && git-receipts --version || echo MISSING
If MISSING, offer — never install unasked, never refuse either.
Detect what's available (command -v brew; command -v cargo) and ask:
brew →
brew install cloudcraft-ai/tap/gitreceipts(preferred)cargo →
cargo install gitreceipts(thengit-receipts man --installonce, sogit receipts --helpworks — cargo installs no man page)both → ask which they prefer
neither → the prebuilt, checksum-verified binary:
cd "$(mktemp -d)" TAG=$(curl -fsSL https://api.github.com/repos/jagmeetchawla/gitreceipts/releases/latest | python3 -c "import json,sys; print(json.load(sys.stdin)['tag_name'])") case "$(uname -s)-$(uname -m)" in Darwin-arm64) T=aarch64-apple-darwin;; Darwin-x86_64) T=x86_64-apple-darwin;; Linux-x86_64) T=x86_64-unknown-linux-musl;; *) T="";; esac A="git-receipts-$TAG-$T.tar.gz" curl -fsSLO "https://github.com/jagmeetchawla/gitreceipts/releases/download/$TAG/$A" curl -fsSLO "https://github.com/jagmeetchawla/gitreceipts/releases/download/$TAG/SHA256SUMS" (shasum -a 256 -c --ignore-missing SHA256SUMS 2>/dev/null || sha256sum -c --ignore-missing SHA256SUMS) | grep "$A" tar xzf "$A" && mkdir -p ~/.local/bin && mv "git-receipts-$TAG-$T/git-receipts" ~/.local/bin/The checksum line must print
OK— never proceed past a failed one.Then, optionally, verify PROVENANCE — which commit and which CI run built that tarball. Probe the SUBCOMMAND, not
ghitself: the attestation command arrived in gh 2.49, and on an older ghcommand -v ghsucceeds while the call dumps a usage screen. A confusing error during a security check is worse than no check, because the user cannot tell whether their DOWNLOAD is suspect or the tool is.if gh attestation --help >/dev/null 2>&1; then gh attestation verify "$A" -R jagmeetchawla/gitreceipts else echo "provenance check skipped — needs gh 2.49+; the checksum above is the gate" fiSay which one you got. The checksum proves the bytes match what was published; the attestation proves who published them. brew and cargo verify integrity on their own, so this is the only route where the user does it by hand — and the route taken by machines with the fewest tools.
If which -a git-receipts shows more than one path, say which one wins and
that the others are shadowed — an older copy earlier on PATH silently
decides everything below.
Version floor — and offering the upgrade
This skill drives views that shipped in binary 0.1.1, and the identity
guidance below (the you: header, --me, --all-authors) needs 0.1.3. If
git-receipts --version reports older, say so ONCE, offer the upgrade for
the route they actually installed by, and carry on with the fallback in
"Older binaries" — never block the answer they asked for.
Detect the route rather than guessing (a cargo copy can sit behind a brew one on PATH):
which -a git-receipts
brew list gitreceipts >/dev/null 2>&1 && echo "route: brew"
[ -x "$HOME/.cargo/bin/git-receipts" ] && echo "route: cargo"
| Route | Offer |
|---|---|
| brew | brew update && brew upgrade gitreceipts |
| cargo | cargo install gitreceipts --force (then git-receipts man --install) |
| prebuilt binary | re-run the download block above — it always fetches the latest release |
Phrase it as an offer, once per conversation, and run it only on an
explicit yes — the same rule as the install. Something like: "this is
binary 0.1.0; recap arrived in 0.1.1, and identity in 0.1.3. Want me to run brew upgrade gitreceipts? I can read the session either way."
Reading a session
The default command is the whole point — one command, one screen:
git receipts recap --no-pager --color never
That prints the headline (commits · prompts · claimed files landed), names any commits worth a look, then one entry per commit: the ask on top, what it became underneath. Present it as a code block — it is pre-aligned — and let it speak. Don't re-narrate what the table already says; add only what it can't: which of these the user probably cares about right now, and why.
Scopes, all composable:
| The user says | Run |
|---|---|
| nothing / "what happened" | git receipts recap (all sessions for this repo) |
| "this session" | git receipts recap --this-session "$MARKER" (below) |
| "yesterday's run", "the last one" | git receipts recap --latest |
| "what happened in commit X" | git receipts recap --commit <hash> |
| "all my repos here" | git receipts recap --project |
| "more detail" | add --verbose (every commit, in full) |
| "include my own commits" | add --full-history |
"This session" needs identity, not a guess: echo a unique marker into the conversation first, then let the binary find the file that contains it.
M="receipts-$$-$(date +%s)"; echo "$M"; sleep 1
git receipts recap --this-session "$M" --no-pager --color never
If it reports no session contains the marker, the log hasn't flushed —
wait a second, retry once, then fall back to --latest and say so.
Whose commits. The header says which git identity the report covers and
how much of the window it matched — you: Ada <ada@example.com> (191 of 204 commits are yours). A shrinking ratio means the repo's user.email isn't
the one that made the commits; --me <name|email> adds another.
Who this is for. An agent with its context intact remembers more than any recap can reconstruct — recap is for whoever wasn't there. That's the user, and it is also this session after compaction, when the receipt hands back a prompt-to-commit chain the conversation no longer holds.
Interpreting for the user
- Lead with the story, not the score. What was asked, what happened, what landed. The numbers support the sentence; they aren't the sentence.
- Never hide a finding. If the headline names commits worth a look, say what they are and offer the audit. A recap that omits problems is a prettier memoir, which is the failure this tool exists to catch.
- The marks are muted on purpose:
·means something is noted (a failed command, a file written and discarded),!means unexplained residue,✘means a claimed edit git never got. Findings are named in the entry — quote the tool's own words rather than inventing a diagnosis. - Iteration is the story, not noise. Drafts superseded, files relocated before their first commit, approaches written and thrown away, a commit amended seconds later — the audit keeps these quiet because they're resolved; a recap is exactly where they belong.
- Never soften a red. It means nothing on record explains the claim — and the user may know something the log doesn't, so present it as a question.
Going deeper
Always end the first recap with the ways in, one line, no menu:
recap <hash>for one commit's whole story ·reportfor a page you can keep ·auditfor the verdict.
When they take one:
git receipts recap --commit <hash> --no-pager --color never # the story
git receipts audit --summary --emoji --no-pager # the verdict
For a page — generate and open in one step, scope-matched to what you just
read, and use --compact so it opens anywhere (a full report is several
hundred KB and chat preview panes choke on it):
F="${TMPDIR:-/tmp}/gitreceipts-recap.html"
git receipts recap <scope args> --format html --compact > "$F" && open "$F"
(xdg-open on Linux.) State the path. It lands OUTSIDE the repo on
purpose — a reading tool must not leave files its own next run would flag.
No browser (SSH, headless)? Still write it, give the path, say why it
didn't open.
Machine-readable twin, unchanged and complete: git receipts export > receipt.json.
Privacy
Reports are built from the user's own prompts and command output — treat
every one as private. Whenever you suggest sharing or exporting, name the
flags in the same breath: --no-intent (drops prompts and agent prose;
every count stays), --no-identity (drops names and emails), --redact <word> (masks anything else). Home paths are masked automatically.
Never pass --no-scan — the secret/PII scanner stays on, always.
Guardrails
- Never install the binary unasked; offer, and let the user choose.
- Never
--no-scan. - Never load a full report into the conversation — the console views are already condensed; HTML and JSON are files humans open.
- Pass through explicit flags the user gives verbatim, but only if they look like flags, paths, or a commit hash — never shell syntax.
Older binaries (before 0.1.1)
recap, --summary, --this-session and --compact don't exist there;
the identity header, --me and --all-authors need 0.1.3.
Do all three things, in this order. The offer is not optional — a degraded answer with no way out leaves the user stuck on an old binary without knowing a fix exists.
Say it once — which view they're getting and why.
Detect the route and OFFER the upgrade, with the actual command:
which -a git-receipts brew list gitreceipts >/dev/null 2>&1 && echo "route: brew" [ -x "$HOME/.cargo/bin/git-receipts" ] && echo "route: cargo"Route Offer brew brew update && brew upgrade gitreceiptscargo cargo install gitreceipts --force(thengit-receipts man --install)prebuilt re-run the download block above Run it only on an explicit yes — never as a side effect of the answer they actually asked for.
Then fall back and answer anyway:
git-receipts audit --oneline --no-pager --color never # the closest view git-receipts audit --latest --no-pager # one session
Something like: "this is binary 0.1.0 — recap arrived in 0.1.1 and the
identity header in 0.1.3, so I'll run the closest view it has. Want me to
run brew update && brew upgrade gitreceipts first? I can read the session
either way."
For "this session", find the file by marker yourself and pass it as an
argument: grep -rl "$M" ~/.claude/projects/*/ | head -1.