@prufs/trail-hooks
v0.1.0
Published
Signed, hash-chained Prufs audit trail for agentic coding sessions. Metadata only, never file contents or prompts.
Maintainers
Readme
@prufs/trail-hooks
Signed, hash-chained audit trails for agentic coding sessions. One command per hook, one receipt per session.
Agentic coding tools edit files and run commands on your behalf. When an examiner or an auditor asks what the agent did, most teams have nothing to show. This package gives Claude Code (and Cursor, see below) a tamper-evident record of each session: what tools ran, how often, against which files, signed with your own Ed25519 key and anchored at api.prufs.ai with a verifiable receipt.
Privacy stance, first
The trail is metadata only. Events carry:
- tool name (Edit, Write, Bash, NotebookEdit)
- a redacted file path (basename by default; full path or a truncated hash if you choose)
- timestamps, counts, and the session id
- exit codes for Bash, when the hook payload exposes one
Events never carry file contents, never prompts, never conversation text, and never command strings. For a Bash step the trail records that Bash ran and whether it failed. It does not record what the command was. This is enforced at the capture site in the code, stated in comments there, and covered by tests that feed secrets through the hooks and assert they never reach disk.
If the trail is not configured, every command exits 0 silently. If anything fails at runtime, the error goes to ~/.prufs/trail-hooks.log and the coding session continues untouched. A hook must never break your session.
Quickstart
npm install -g @prufs/trail-hooks
prufs-trail initinit writes ~/.prufs/trail-hooks.json, generates an Ed25519 keypair at ~/.prufs/trail-hooks.pem (owner-read-only), and prints your signer id plus the exact curl to register the public key with your Prufs org. Registration needs an org admin key; day-to-day flushes need only an ingest key:
export PRUFS_API_KEY=<your org ingest key>Then merge the ready-made hooks block into your ~/.claude/settings.json (or a project's .claude/settings.json). The block ships in this package at hooks/claude-code.json:
SessionStartrunsprufs-trail event session_startedPostToolUse(matcherEdit|Write|NotebookEdit|Bash) runsprufs-trail event tool_useStoprunsprufs-trail event session_ended && prufs-trail flush
That is the whole integration. Events buffer locally per session in ~/.prufs/trail-sessions/. On flush they are coalesced (repeated edits to one file become one event with a count, so a session always fits the 100-event batch cap), signed into a hash chain with genesis prev_hash "0", and shipped as one batch. The receipt lands in ~/.prufs/trail-receipts/<session_id>.json.
Claude Code fires Stop at the end of every turn, so a multi-turn session produces one genesis batch and then continuation batches. Each continuation chains from the stored session head and the server verifies the link, so the whole session still reads as one continuous chain that opens with session_started and closes with session_ended.
Verifying a receipt
prufs-trail verify <session_id>This recomputes every content hash and Ed25519 signature locally with @prufs/sdk (the same verifyEvent the server trusts) and checks the chain links, including across batches. With PRUFS_API_KEY set it also fetches each server receipt from GET /v1/event-receipts/:id and confirms the stored head matches your local head. A receipt with verification_status: verified_ed25519_hash_chain means the server checked the same math before storing.
Anyone with your public key can verify the chain independently. The signing contract is @prufs/sdk 0.2.2: content_hash is the SHA-256 of the canonical JSON (sorted keys) of the six unsigned fields, and the signature is Ed25519 over utf8(content_hash + prev_hash).
Cursor
Cursor has a native hooks system (~/.cursor/hooks.json or <project>/.cursor/hooks.json) with sessionStart, afterFileEdit, beforeShellExecution, and stop events that pipe JSON to a command, the same shape of integration as Claude Code. The payload field names differ (conversation_id rather than session_id, hook_event_name casing, per-edit diffs in afterFileEdit), so the Claude Code adapter does not read them yet. A hooks/cursor.json adapter is the next milestone; until then Cursor users can call prufs-trail event from a small wrapper script that maps conversation_id to session_id and strips the edits array before piping.
Why
The Federal Reserve's SR 26-2 flagged the gap between what firms claim about generative AI controls and the evidence they can produce. Agentic coding sits squarely in that gap: the work is real, the record usually is not. A signed, hash-chained trail with server receipts is evidence you can hand over, and because it is metadata only, handing it over discloses nothing about your code.
Commands
| Command | What it does |
|---|---|
| prufs-trail init | Write config, generate the signing key, print signer id and registration curl |
| prufs-trail event <type> | Hook entry point; buffers one metadata event from stdin |
| prufs-trail flush [session_id] | Sign, chain, and ship buffered events; store the receipt. Without an argument, flushes every session that has ended. With one, forces a flush |
| prufs-trail verify <session_id> | Recompute the chain locally; fetch server receipts when credentials exist |
Configuration
~/.prufs/trail-hooks.json:
{
"apiBase": "https://api.prufs.ai",
"apiKeyEnv": "PRUFS_API_KEY",
"signingKeyPath": "~/.prufs/trail-hooks.pem",
"redaction": "basename",
"projectId": null
}redaction is basename (default), full, or hash. projectId null means the project id is the basename of the session's working directory. Set PRUFS_TRAIL_HOME to relocate all state away from ~/.prufs.
License
Apache-2.0.
