docs(blog): Native Tool Calls in the Audit Trail#444
Draft
ojongerius wants to merge 9 commits into
Draft
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new blog post draft explaining how the Claude Code PostToolUse hook (agent-receipts-hook) captures native tool calls (Bash/Read/Write/Edit/etc.) and forwards them to agent-receipts-daemon, complementing MCP proxy coverage.
Changes:
- Add new blog post: “Native Tool Calls in the Audit Trail”.
- Add the post to the site’s Blog sidebar navigation.
- Add a
0.10.0section to the daemon changelog.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| site/src/content/docs/blog/hooks-native-tool-audit.mdx | New blog post describing the Claude Code hook approach and showing example receipts. |
| site/astro.config.mjs | Adds the new blog post to the Blog sidebar items. |
| daemon/CHANGELOG.md | Adds a 0.10.0 changelog entry describing recent daemon changes. |
Comments suppressed due to low confidence (2)
site/src/content/docs/blog/hooks-native-tool-audit.mdx:126
- The JSON example is presented as a “real Bash receipt”, but its shape doesn’t match the repository’s documented receipt schema (e.g., other docs/spec show
issuer+credentialSubject.{action,outcome,chain}withchain.previous_receipt_hash). As written, this looks like a partial/extracted view but it isn’t labeled as such. Consider either showing a full receipt matching the schema, or explicitly labeling this as an excerpt and keeping field names/locations consistent with the spec to avoid confusing readers/copy-pasting.
---
## What a native tool receipt looks like
Here's a real `Bash` receipt from a Claude Code session, captured while running a shell command that included a fake API key:
```json
{
"action": {
"type": "claude-code.Bash",
"tool_name": "Bash",
"risk_level": "medium",
"parameters_hash": "sha256:dc143db4ad2fd5df10685749c688e018610dfb254bd75bbf753ed3ddd84d97a2",
"parameters_disclosure": {
"input": "{\"command\":\"echo \\\"Connecting with api_key=[REDACTED] to service\\\"\"}",
"output": "{\"stdout\":\"Connecting with api_key=[REDACTED] to service\",\"stderr\":\"\"}",
"peer.platform": "darwin",
"peer.uid": "501",
"peer.pid": "74389"
}
},
"outcome": { "status": "success" },
"chain": {
site/src/content/docs/blog/hooks-native-tool-audit.mdx:160
- This section states the hook “exits non-zero” when the daemon is unreachable. Elsewhere in the docs the hook is described as “always exits 0” (e.g.,
site/src/content/docs/reference/cli-commands.mdxunderagent-receipts-hook). Please align the documentation (either update the other page(s) or add a version/behavior note here) so readers don’t get conflicting guidance about failure behavior.
</Aside>
---
## Fail-hard, not silent
Comment on lines
+1
to
+12
| --- | ||
| title: "Native Tool Calls in the Audit Trail" | ||
| description: "How the Claude Code hook captures Bash, Read, Write, Edit and other native tools — closing the gap the MCP proxy leaves open." | ||
| --- | ||
|
|
||
| import { Aside } from '@astrojs/starlight/components'; | ||
|
|
||
| **Series: Auditing AI Agents** · Part 3 of 3 · [← One Chain, Two Channels, Zero Secrets](/blog/unified-chain-redaction-demo/) | ||
|
|
||
| --- | ||
|
|
||
| The MCP proxy covers MCP tool calls. Everything that flows through an MCP server — GitHub API calls, database queries, Atlassian writes — is intercepted, receipted, and hash-chained. But Claude Code has another class of tools that never touch an MCP server at all. |
Comment on lines
+46
to
+47
| - Bump `github.com/agent-receipts/ar/sdk/go` to `v0.9.1` | ||
| (DESC ordering and no silent 10k row cap in `QueryReceipts`). |
Comment on lines
+102
to
+103
| That's it. No MCP server to wrap, no proxy config to write. The next tool call you make will land a receipt. | ||
|
|
2 tasks
45691a1 to
9d78b89
Compare
Two edits to align with the series register: - Add publish date (2026-05-28, one week after post 2 per the weekly cadence). - Soften the closing — "an agent that could previously obscure its activity" framed the agent as adversarial in the same shape as the "compromised or misbehaving agent" line we flattened in post 1. Replace with a structural-completeness framing: what's missing without the hook (file writes, shell commands, web fetches) and what is present with both. Things I considered but think work as-is: - The `Bash`. `Read`. `Write`. ... staccato is name-the-things presentation, not abstract negation. - The "That's it. No MCP server to wrap, no proxy config to write." in the install section caps a contrast against real prior setup pain. - The triple "If it..." in the PreToolUse rationale enumerates real failure modes rather than rhetorical buildup.
Two related fixes that came out of the same review: - **Receipt JSON example** (`hooks-native-tool-audit.mdx`): the example was missing the `credentialSubject` envelope and the `proof` block, flattening fields that the schema nests. Match post 1's level of detail by wrapping `outcome` and `chain` in `credentialSubject` and adding an abbreviated `proof` block. Also abbreviate the `parameters_hash` value to match post 1's convention. Reader no longer sees a partial-shape receipt presented as the canonical example. - **Hook exit-behaviour docs** (`reference/cli-commands.mdx`): the page claimed the hook "Always exits 0" and that emit failures "are dropped silently." That's not what the code does — `agent-receipts-hook` runs with `emitter.WithStrictErrors()` and exits 1 with a stderr message on emit failure. ADR-0010's "events truly drop silently" is about the in-chain ledger (no daemon to record the gap), not about the hook process itself. Reword to describe both: stdin/format issues exit 0 silently, emit failures exit 1 with stderr; the in-chain gap is still silent by design.
Comment on lines
246
to
252
| | Flag | Description | | ||
| |---|---| | ||
| | `--format` | Force a specific input format (default: auto-detected from environment variables). Currently supported: `claude-code`. | | ||
|
|
||
| **Exit behaviour:** Always exits 0. Emit failures (daemon not running, socket missing, malformed frame) are dropped silently — the hook never blocks the agent. | ||
| **Exit behaviour:** The hook never blocks the agent — the tool call has already completed by the time `PostToolUse` fires. Stdin/format issues exit 0 silently (unreadable stdin, unrecognised runtime). Emit failures (daemon unreachable, parse error, socket write timeout) exit 1 with a message on stderr so the operator can surface a missing-receipt event out-of-band. The in-chain audit gap from a daemon-down event is still silent by design — see [ADR-0010](https://github.com/agent-receipts/ar/blob/main/docs/adr/0010-daemon-process-separation.md) — but the hook itself is loud about its own failures. | ||
|
|
||
| **Auto-detection:** When `--format` is omitted, the binary inspects environment variables to identify the calling runtime. `CLAUDE_SESSION_ID` set → `claude-code` format. |
Comment on lines
+176
to
+184
| The recommended Agent Receipts configuration wires up `PostToolUse` only. That's deliberate. | ||
|
|
||
| **Audit first, policy second.** `PreToolUse` can block a tool call by exiting non-zero — which puts the hook in the critical path. If it's slow, the agent is slow. If it crashes, the tool call fails. If it incorrectly blocks something, the agent breaks in ways that are hard to debug. `PostToolUse` has none of these failure modes: the tool has already run, the hook fires after the fact, and a failure to record surfaces as a non-blocking error rather than a broken tool call. The audit trail has a gap, not a breakage. | ||
|
|
||
| **The output is more interesting than the intent.** A `PostToolUse` hook sees what the tool *returned*. A `PreToolUse` hook only sees what the agent *asked for*. For forensics and breach investigation, the output — what actually came back — is often the more useful half of the record. | ||
|
|
||
| **Expanding deliberately.** `SessionStart`, `UserPromptSubmit`, and `Stop` are natural next candidates: they would let the chain record session boundaries and the prompts that triggered tool use, not just the tool calls themselves. Each new event type adds surface area, and there's a wire-format question to resolve first — see the note below. | ||
|
|
||
| The MCP proxy already does `PreToolUse`-equivalent blocking for MCP calls. The hook will follow the same path — audit baseline first, then policy enforcement once it's proven in production. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stub for the third post in the series. Outline is in the file as comments.
Publishing order:
Content to write: the gap MCP proxy leaves (native tools invisible), how the hook fills it, installation (one settings.json change), real Bash receipt, PostToolUse-only today with PreToolUse on the roadmap.