A Claude Code Audit Log: Recording Every Tool Call With PreToolUse and PostToolUse
How to keep an audit log of what Claude Code did, using the PreToolUse and PostToolUse hooks, and what makes a log an audit record rather than just a file you could have edited.
Last updated:
A log is a claim about the past that anyone with write access can revise. That is fine for debugging and not fine for anything you need to show someone else.
This page covers how to record every Claude Code tool call, and what has to be true before that recording deserves the word “audit”.
The two hook events
Claude Code fires hooks on tool-call events and passes the payload to your command on stdin as JSON: the tool name, its input, its response, the session id.
PreToolUsefires before the tool runs. It records the decision: what the agent was about to do, and with what arguments.PostToolUsefires after. It records the outcome: what came back.
Most setups record only the second one. That gives you a history of results, which answers “what happened” and cannot answer “was this authorised before it ran”. Those are different questions, and only the second one survives a challenge.
PostToolUse also cannot block anything, because the tool has already executed by the time it runs. If you want a record that says a denial was actually enforced, the denial has to be recorded in the phase where enforcement was still possible.
Setting it up
pip install homestead-memory # needs Python 3.10+
hsm hook --install # prints a snippet for ~/.claude/settings.json
The snippet registers both phases with matcher "*" and an explicit five second timeout. It is printed, not written: turning on a recording of everything you do should be something you read first and paste yourself.
The default hook timeout in Claude Code is 600 seconds, which means a hung hook could stall a session for ten minutes. The snippet sets its own timeout instead of inheriting that.
hsm watch # read it back, decision and outcome in order
hsm watch --tool Bash # filter to one tool
hsm watch --session <id> # filter to one session
hsm watch --json # machine-readable, valid JSON on every failure path
What makes it an audit log
Three properties, in increasing order of how much they cost to fake.
One: it is append-only and hash-chained. Every record carries the hash of the record before it. Edit a record, delete one, or swap two, and every hash after that point stops matching. hsm watch reports the break at the exact index rather than telling you something is vaguely wrong.
Two: the head is signed. Hash chaining alone has a hole worth naming: someone who rewrites every record can recompute every hash and produce a file that is internally perfect. hsm checkpoint signs the head with a key, which catches that. Everything appended after your last checkpoint is not yet covered, so run it regularly.
Three: the record can leave with the reader. hsm export --evidence produces a pack that a third party verifies using the Python standard library alone. They install nothing, and they take nothing from us on trust. If verifying a record requires the tool that produced it, the reader is trusting the producer to vouch for itself.
What it does not give you
- It records what the harness reported. If a tool claims success and did nothing, you get a hash-chained record of that claim. This is evidence of what was reported, not independent observation.
- The signing key sits on the same machine as the log. Whoever can rewrite the file can usually read the key.
hsm checkpoint --exportprints one line you can publish somewhere they do not control, which is the answer to that. - Claude Code only. Cursor and Codex use different mechanisms and are not supported.
- Nothing that never reached the hook is in the log, including a hook you did not install and any event the tool recorded as dropped.
The point of listing those is that an audit log whose limits are unstated is worse than no audit log, because it invites trust it has not earned.
github.com/fuckbigtech-ai/homestead-memory
For the wider comparison against hosted observability platforms, see how to monitor Claude Code.
Quick Answers
What is the difference between PreToolUse and PostToolUse?
PreToolUse fires before the tool runs and records the decision. PostToolUse fires after and records the outcome. Recording only the outcome tells you what happened. Recording both tells you what was authorised before it ran.
Does a hook slow Claude Code down?
Yes. Both phases together cost about 124ms per tool call, measured, median. On a 100-call session that is roughly twelve seconds. Most of it is Python interpreter startup.
Can a PostToolUse hook block a tool call?
No. PostToolUse runs after the tool has already executed, so it cannot prevent anything. That is exactly why recording the decision phase separately matters.
What stops someone editing the log afterwards?
Nothing stops it. A hash chain makes it detectable: each record carries the hash of the one before, so an edit, a deletion or a reorder breaks every hash after it and is reported at the exact index.