From d11e1009b141671d0c02837163348791f2ebaa7d Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Sun, 17 May 2026 09:45:12 +1200 Subject: [PATCH 1/9] =?UTF-8?q?docs(blog):=20One=20Chain,=20Two=20Channels?= =?UTF-8?q?,=20Zero=20Secrets=20=E2=80=94=20unified=20chain=20and=20redact?= =?UTF-8?q?ion=20demo=20post?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/astro.config.mjs | 4 + .../blog/unified-chain-redaction-demo.mdx | 160 ++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 site/src/content/docs/blog/unified-chain-redaction-demo.mdx diff --git a/site/astro.config.mjs b/site/astro.config.mjs index b508dfd2..4d323d33 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -168,6 +168,10 @@ export default defineConfig({ label: "The audit boundary belongs outside the agent", slug: "blog/daemon-process-separation", }, + { + label: "One Chain, Two Channels, Zero Secrets", + slug: "blog/unified-chain-redaction-demo", + }, ], }, ], diff --git a/site/src/content/docs/blog/unified-chain-redaction-demo.mdx b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx new file mode 100644 index 00000000..b6278bdf --- /dev/null +++ b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx @@ -0,0 +1,160 @@ +--- +title: "One Chain, Two Channels, Zero Secrets: A Live Demo" +description: "How the Claude Code hook and MCP proxy write to the same auditable chain — and how secrets are redacted before they ever reach storage." +--- + +import { Aside } from '@astrojs/starlight/components'; + +This post walks through two things that landed together in `agent-receipts-daemon v0.10.0`: a unified receipt chain that captures both native Claude Code tool calls and MCP tool calls in a single sequence, and automatic secret redaction before any payload reaches the database. + +Both were validated live. The receipts below are real. + +--- + +## The problem with two stores + +Before v0.10.0, the picture looked like this: + +- The **MCP proxy** signed and stored receipts in its own `receipts.db` +- The **Claude Code hook** forwarded events to the daemon, which stored them in the daemon's `receipts.db` + +Two databases. Two chains. No way to correlate a native `Bash` call with the MCP tool call that triggered it, within the same audit trail. + +ADR-0010 (Daemon Process Separation) was always meant to fix this: one daemon, one chain, all emitters forwarding events to it. v0.10.0 completes that picture. + +--- + +## One call, two receipts, one chain + +Here's what happens when Claude Code makes a GitHub MCP call today. The proxy intercepts it (it's an MCP call over stdio) and the hook fires after it completes (it's a PostToolUse event). Both emitters forward to the daemon, which assigns consecutive sequence numbers. + +**Sequence 2046 — from the MCP proxy:** + +```json +{ + "action": { + "type": "mcp.github.pull_request_read", + "tool_name": "pull_request_read", + "parameters_hash": "sha256:2f9911eb87456bd0e3bffe3834529e18300cd5ca311547a8e45717992303c70f" + }, + "chain": { + "sequence": 2046, + "chain_id": "default" + } +} +``` + +**Sequence 2047 — from the Claude Code hook:** + +```json +{ + "action": { + "type": "claude-code.mcp__github-audited__pull_request_read", + "tool_name": "mcp__github-audited__pull_request_read", + "parameters_hash": "sha256:2f9911eb87456bd0e3bffe3834529e18300cd5ca311547a8e45717992303c70f" + }, + "chain": { + "sequence": 2047, + "chain_id": "default" + } +} +``` + +Same `parameters_hash`. Same timestamp. Consecutive sequence numbers. Different emitters — one the proxy (pid 53707), one the hook (pid 74061) — both attested by the daemon's peer credential capture. + +The `action.type` prefix tells you which channel produced each receipt: `mcp.*` for the proxy, `claude-code.*` for the hook. + + + +--- + +## Peer attestation + +Both receipts include `parameters_disclosure.peer.*` — the OS-level credentials the daemon captured at connection time, independent of anything the emitter claims: + +```json +"parameters_disclosure": { + "peer.platform": "darwin", + "peer.uid": "501", + "peer.gid": "20", + "peer.pid": "53707", + "peer.exe_path": "" +} +``` + +The daemon uses `LOCAL_PEERCRED` + `LOCAL_PEEREPID` on macOS to capture these at socket accept time. The emitter cannot forge them. `exe_path` is empty for the hook channel because the hook process exits before the daemon can read its path from `proc_pidpath` — a known limitation for short-lived processes. + +--- + +## Secret redaction + +The `parameters_disclosure` feature has a history. It started as `parameterPreview` inside the [OpenClaw plugin](https://github.com/agent-receipts/openclaw) — a way to store a sanitised preview of tool call parameters alongside the hash, so auditors could see *what* was called without needing to re-run the tool ([how the plugin works](/blog/openclaw-plugin-deep-dive/)). The [first end-to-end trial](https://jongerius.solutions/post/agent-receipts-openclaw-v0-trial/) used it in anger. Using it surfaced an immediate question: what happens when a tool call argument contains an API key or a password? The preview stores it. Forever. In a signed record. + +That question drove two things: the rename from `parameterPreview` → `parameterDisclosure` (the field carries disclosure, not a safe preview), and the redaction system now in the daemon. The idea was ported from the plugin to the main project precisely because the daemon is the right place for it — it's the only process that sees the raw payload, making it the only place where redaction can run unconditionally, regardless of which emitter sent the event. + +With `--parameter-disclosure` enabled, the daemon stores plaintext inputs and outputs in the receipt's `parameters_disclosure` field. Without redaction, this would be a liability: any secret that appears in a tool call argument or response would be persisted forever in a signed, immutable record. + +v0.10.0 adds redaction to the pipeline. Built-in patterns cover: + +- GitHub PATs (`ghp_`, `github_pat_`, `gho_`, `ghs_`, `ghu_`) +- OpenAI / Anthropic keys (`sk-...`) +- AWS access key IDs (`AKIA...`) +- Bearer tokens in HTTP headers +- Slack tokens (`xox[bpras]-...`) +- PEM private key blocks +- URL query-string tokens (`?token=`, `?api_key=`, ...) +- JSON keys: `password`, `api_key`, `token`, `secret`, `authorization`, `jwt`, and 20+ more + +Redaction runs **after** hashing. The `parameters_hash` commits to the original canonical bytes — a verifier can still prove the exact inputs were used. Only the human-readable stored text is sanitised. + +Here's the live demo. This Bash command was run in a Claude Code session: + +```bash +echo "Connecting with api_key=sk-fakekey12345678901234 to service" +``` + +The resulting receipt: + +```json +"parameters_disclosure": { + "input": "{\"command\":\"sleep 1 && echo \\\"Connecting with api_key=[REDACTED] to service\\\"\",\"description\":\"Trigger tool call with fake API key in output\"}", + "output": "{\"stdout\":\"Connecting with api_key=[REDACTED] to service\",\"stderr\":\"\",\"interrupted\":false}", + "peer.platform": "darwin", + "peer.uid": "501", + "peer.pid": "74389" +} +``` + +The secret is gone from both the command string and the stdout — caught in both places by the `sk-` pattern. The `parameters_hash` still proves what was actually executed. The receipt is signed and chain-linked. + +Custom redaction patterns can be added via `--redact-patterns ` (or `AGENTRECEIPTS_REDACT_PATTERNS`): + +```yaml +patterns: + - name: internal-api-key + pattern: 'MYCO-[A-Z0-9]{16}' +``` + +--- + +## What this enables + +Every native tool call and every MCP tool call from a Claude Code session now lands in one chain, hash-linked and signed, with secrets sanitised before storage. An auditor can: + +- Query by `action.type` prefix to filter by channel (`claude-code.*` vs `mcp.*`) +- Correlate cross-channel calls by `parameters_hash` when both channels capture the same event +- Verify any receipt's signature and hash chain offline with `agent-receipts verify` +- Inspect recent receipts with `agent-receipts list --json | jq ...` + +The daemon is the only process that ever sees raw inputs — emitters are fire-and-forget. If the daemon is down, the hook logs a visible non-blocking error and the tool call completes normally; no receipt is written for that call and the gap is visible in the chain on restart. + +--- + +## What's next + +- **OpenClaw on the shared chain** — openclaw emitting to the daemon socket, adding a third channel to the unified trail +- **Breach-indicator flagging** (#436) — daemon-side pattern matching at emit time, writing indexed flags alongside receipts for efficient breach investigation queries +- **Daemon config file** (#441) — replacing plist/unit file edits with a single `daemon.toml` +- **`parameters_disclosure` encryption** (#280) — replacing plaintext disclosure with asymmetric ciphertext so only the forensic key holder can read stored inputs From 279e4b4c147a6e51cfe4b48c85bf8d2eaf41b47e Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Sun, 17 May 2026 09:48:23 +1200 Subject: [PATCH 2/9] =?UTF-8?q?docs(blog):=20fix=20what's=20next=20?= =?UTF-8?q?=E2=80=94=20openclaw=20already=20on=20shared=20chain;=20central?= =?UTF-8?q?ised=20storage=20is=20the=20real=20next=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/content/docs/blog/unified-chain-redaction-demo.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/content/docs/blog/unified-chain-redaction-demo.mdx b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx index b6278bdf..b3138a10 100644 --- a/site/src/content/docs/blog/unified-chain-redaction-demo.mdx +++ b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx @@ -154,7 +154,7 @@ The daemon is the only process that ever sees raw inputs — emitters are fire-a ## What's next -- **OpenClaw on the shared chain** — openclaw emitting to the daemon socket, adding a third channel to the unified trail -- **Breach-indicator flagging** (#436) — daemon-side pattern matching at emit time, writing indexed flags alongside receipts for efficient breach investigation queries +- **Centralised storage** — the daemon today writes to a local SQLite file. The natural next step is a remote sink: ship receipts from multiple agents and machines to a shared store — Postgres, S3, or an OTel-compatible backend — so cross-agent audit trails can be correlated in one place +- **Breach-indicator flagging** (#436) — daemon-side pattern matching at emit time, writing indexed flags alongside receipts for efficient breach investigation queries without needing to decrypt stored payloads - **Daemon config file** (#441) — replacing plist/unit file edits with a single `daemon.toml` -- **`parameters_disclosure` encryption** (#280) — replacing plaintext disclosure with asymmetric ciphertext so only the forensic key holder can read stored inputs +- **`parameters_disclosure` encryption** (#280) — replacing plaintext disclosure with asymmetric ciphertext so only the forensic key holder can read stored inputs; the redaction layer ships first so secrets are never written even in the interim From d1e048a4e26c191c12fb202f8f532f78f78d219d Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Sun, 17 May 2026 09:49:45 +1200 Subject: [PATCH 3/9] docs(blog): add sequence diagrams for unified chain and redaction pipeline --- .../blog/unified-chain-redaction-demo.mdx | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/site/src/content/docs/blog/unified-chain-redaction-demo.mdx b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx index b3138a10..908e0b75 100644 --- a/site/src/content/docs/blog/unified-chain-redaction-demo.mdx +++ b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx @@ -28,6 +28,29 @@ ADR-0010 (Daemon Process Separation) was always meant to fix this: one daemon, o Here's what happens when Claude Code makes a GitHub MCP call today. The proxy intercepts it (it's an MCP call over stdio) and the hook fires after it completes (it's a PostToolUse event). Both emitters forward to the daemon, which assigns consecutive sequence numbers. +```mermaid +sequenceDiagram + participant CC as Claude Code + participant MP as mcp-proxy + participant MS as MCP Server + participant Hook as agent-receipts-hook + participant D as agent-receipts-daemon + participant DB as receipts.db + + CC->>MP: tool call (stdio) + MP->>MS: forward (stdio) + MS-->>MP: response + MP-->>CC: response + MP--)D: emit frame · channel: mcp + CC--)Hook: PostToolUse event + Hook--)D: emit frame · channel: claude-code + Note over D: redact secrets in error / disclosure fields + D->>DB: INSERT seq 2046 · mcp.github.pull_request_read + D->>DB: INSERT seq 2047 · claude-code.mcp__github-audited__pull_request_read +``` + +Both frames carry the same `parameters_hash` — the daemon hashes inputs canonically before any emitter can tamper with them. The dashed arrows (fire-and-forget) mean neither emitter blocks waiting for the receipt to land. + **Sequence 2046 — from the MCP proxy:** ```json @@ -96,6 +119,22 @@ That question drove two things: the rename from `parameterPreview` → `paramete With `--parameter-disclosure` enabled, the daemon stores plaintext inputs and outputs in the receipt's `parameters_disclosure` field. Without redaction, this would be a liability: any secret that appears in a tool call argument or response would be persisted forever in a signed, immutable record. +```mermaid +sequenceDiagram + participant E as Emitter + participant D as agent-receipts-daemon + participant DB as receipts.db + + E--)D: raw input · "api_key=sk-abc123..." + Note over D: 1 · canonicalise → parameters_hash + Note over D: 2 · redact → "api_key=[REDACTED]" + Note over D: 3 · sign receipt + D->>DB: parameters_hash: sha256:... + D->>DB: parameters_disclosure.input: "api_key=[REDACTED]" +``` + +The hash in step 1 commits to the original bytes. An auditor who later obtains the raw input can verify it matches. The redacted text in step 2 is what gets signed and stored — the secret never reaches the database. + v0.10.0 adds redaction to the pipeline. Built-in patterns cover: - GitHub PATs (`ghp_`, `github_pat_`, `gho_`, `ghs_`, `ghu_`) From 533217f7813779b0aceeb3e5b1629577cb6541d5 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Sun, 17 May 2026 13:19:42 +1200 Subject: [PATCH 4/9] =?UTF-8?q?docs(blog):=20tighten=20unified-chain=20pos?= =?UTF-8?q?t=20=E2=80=94=20fix=20exe=5Fpath=20explanation=20and=20command/?= =?UTF-8?q?receipt=20mismatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/content/docs/blog/index.mdx | 1 + .../docs/blog/unified-chain-redaction-demo.mdx | 12 +++++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/site/src/content/docs/blog/index.mdx b/site/src/content/docs/blog/index.mdx index 7d2ccd15..81deed90 100644 --- a/site/src/content/docs/blog/index.mdx +++ b/site/src/content/docs/blog/index.mdx @@ -7,6 +7,7 @@ Updates, deep dives, and field notes from building the Agent Receipt Protocol. ## Posts +- **2026-05-21** — [One Chain, Two Channels, Zero Secrets: A Live Demo](/blog/unified-chain-redaction-demo/) — How the Claude Code hook and MCP proxy write to the same auditable chain, and how secrets are redacted before they ever reach storage. - **2026-05-17** — [The audit boundary belongs outside the agent](/blog/daemon-process-separation/) — The architectural problem with in-process receipt signing, and how daemon process separation restores the audit property. - **2026-04-27** — [OpenClaw Plugin: How It Works](/blog/openclaw-plugin-deep-dive/) — Plugin architecture, receipt anatomy, self-verification, and how the OpenClaw integration differs from the MCP Proxy. - **2026-04-16** — [Agent Security Tooling Landscape — April 2026](/blog/agent-security-tooling-landscape-april-2026/) — MCP gateways, agent firewalls, kernel enforcement, governance frameworks, and the convergence of primitives like Ed25519, SHA-256 chaining, and DIDs. diff --git a/site/src/content/docs/blog/unified-chain-redaction-demo.mdx b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx index 908e0b75..1e93d615 100644 --- a/site/src/content/docs/blog/unified-chain-redaction-demo.mdx +++ b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx @@ -83,7 +83,7 @@ Both frames carry the same `parameters_hash` — the daemon hashes inputs canoni } ``` -Same `parameters_hash`. Same timestamp. Consecutive sequence numbers. Different emitters — one the proxy (pid 53707), one the hook (pid 74061) — both attested by the daemon's peer credential capture. +Same `parameters_hash`. Consecutive sequence numbers, milliseconds apart. Different emitters — one the proxy (pid 53707), one the hook (pid 74061) — both attested by the daemon's peer credential capture. The `action.type` prefix tells you which channel produced each receipt: `mcp.*` for the proxy, `claude-code.*` for the hook. @@ -107,13 +107,13 @@ Both receipts include `parameters_disclosure.peer.*` — the OS-level credential } ``` -The daemon uses `LOCAL_PEERCRED` + `LOCAL_PEEREPID` on macOS to capture these at socket accept time. The emitter cannot forge them. `exe_path` is empty for the hook channel because the hook process exits before the daemon can read its path from `proc_pidpath` — a known limitation for short-lived processes. +The daemon uses `LOCAL_PEERCRED` + `LOCAL_PEEREPID` on macOS to capture these at socket accept time. The emitter cannot forge them. `exe_path` is empty when the peer has already closed its end of the socket by the time the daemon resolves it: `LOCAL_PEEREPID` returns `ENOTCONN`, the pid is left at zero, and `proc_pidpath` is skipped. Short-lived emitters like the hook (one connection per frame, then exit) hit this path often; `uid`/`gid` still come through because `LOCAL_PEERCRED` snapshots them at connect time. --- ## Secret redaction -The `parameters_disclosure` feature has a history. It started as `parameterPreview` inside the [OpenClaw plugin](https://github.com/agent-receipts/openclaw) — a way to store a sanitised preview of tool call parameters alongside the hash, so auditors could see *what* was called without needing to re-run the tool ([how the plugin works](/blog/openclaw-plugin-deep-dive/)). The [first end-to-end trial](https://jongerius.solutions/post/agent-receipts-openclaw-v0-trial/) used it in anger. Using it surfaced an immediate question: what happens when a tool call argument contains an API key or a password? The preview stores it. Forever. In a signed record. +The `parameters_disclosure` feature has a history. It started as `parameterPreview` inside the [OpenClaw plugin](https://github.com/agent-receipts/openclaw) — a way to store a sanitised preview of tool call parameters alongside the hash, so auditors could see *what* was called without needing to re-run the tool ([how the plugin works](/blog/openclaw-plugin-deep-dive/)). The [first end-to-end trial](https://jongerius.solutions/post/agent-receipts-openclaw-v0-trial/) put it to work in anger, and an obvious question surfaced almost immediately: what happens when a tool call argument contains an API key or a password? The preview stores it. Forever. In a signed record. That question drove two things: the rename from `parameterPreview` → `parameterDisclosure` (the field carries disclosure, not a safe preview), and the redaction system now in the daemon. The idea was ported from the plugin to the main project precisely because the daemon is the right place for it — it's the only process that sees the raw payload, making it the only place where redaction can run unconditionally, regardless of which emitter sent the event. @@ -146,12 +146,10 @@ v0.10.0 adds redaction to the pipeline. Built-in patterns cover: - URL query-string tokens (`?token=`, `?api_key=`, ...) - JSON keys: `password`, `api_key`, `token`, `secret`, `authorization`, `jwt`, and 20+ more -Redaction runs **after** hashing. The `parameters_hash` commits to the original canonical bytes — a verifier can still prove the exact inputs were used. Only the human-readable stored text is sanitised. - Here's the live demo. This Bash command was run in a Claude Code session: ```bash -echo "Connecting with api_key=sk-fakekey12345678901234 to service" +sleep 1 && echo "Connecting with api_key=sk-fakekey12345678901234 to service" ``` The resulting receipt: @@ -166,7 +164,7 @@ The resulting receipt: } ``` -The secret is gone from both the command string and the stdout — caught in both places by the `sk-` pattern. The `parameters_hash` still proves what was actually executed. The receipt is signed and chain-linked. +The secret is gone from both the command string and the stdout — caught in both places by the `sk-` pattern. The receipt is signed and chain-linked; the hash still commits to the original input, but the redacted form is the only text the database, and any subsequent reader, ever sees. Custom redaction patterns can be added via `--redact-patterns ` (or `AGENTRECEIPTS_REDACT_PATTERNS`): From 216eeb66e2c75e859a4cffcb9af76a5a7449e437 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Sun, 17 May 2026 13:24:09 +1200 Subject: [PATCH 5/9] docs(blog): add series framing to redaction demo post --- site/src/content/docs/blog/unified-chain-redaction-demo.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/site/src/content/docs/blog/unified-chain-redaction-demo.mdx b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx index 1e93d615..422730da 100644 --- a/site/src/content/docs/blog/unified-chain-redaction-demo.mdx +++ b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx @@ -5,6 +5,10 @@ description: "How the Claude Code hook and MCP proxy write to the same auditable import { Aside } from '@astrojs/starlight/components'; +**Series: Auditing AI Agents** · Part 2 of 3 · [← Why Your Agent Can't Audit Itself](/blog/daemon-process-separation/) · [Native Tool Calls in the Audit Trail →](/blog/hooks-native-tool-audit/) + +--- + This post walks through two things that landed together in `agent-receipts-daemon v0.10.0`: a unified receipt chain that captures both native Claude Code tool calls and MCP tool calls in a single sequence, and automatic secret redaction before any payload reaches the database. Both were validated live. The receipts below are real. From f1e075f921071272d0aed319ce639a1ff753693c Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Sun, 17 May 2026 19:48:22 +1200 Subject: [PATCH 6/9] docs(blog): tone pass and forward-link strip on redaction-demo post MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same treatment as post 1: remove broken forward references, soften a few rhetorical shapes, and reframe the exe_path failure mode positively. - Strip the forward link to post 3 (not shipped yet) and update the backward link to use post 1's new title. Add a publish date. - Drop "Both were validated live" buildup before the real receipts. - Flatten "Two databases. Two chains. No way to correlate..." rule-of- three into one sentence. - Reframe the peer-credential paragraph: lead with uid/gid as the always-works property, then describe pid/exe_path as best-effort by design rather than a bug. Closer: "The kernel always wins on uid/gid; pid/exe_path is best-effort by design." - Soften the duplicate "persisted forever in a signed, immutable record" line; the earlier "Forever. In a signed record." in the history is the dramatic punch — restating it is overkill. - Trim "What's next" — drop the tactical daemon.toml item and reorder so architectural roadmap (storage, encryption) leads. --- .../docs/blog/unified-chain-redaction-demo.mdx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/site/src/content/docs/blog/unified-chain-redaction-demo.mdx b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx index 422730da..0e0a976d 100644 --- a/site/src/content/docs/blog/unified-chain-redaction-demo.mdx +++ b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx @@ -5,13 +5,13 @@ description: "How the Claude Code hook and MCP proxy write to the same auditable import { Aside } from '@astrojs/starlight/components'; -**Series: Auditing AI Agents** · Part 2 of 3 · [← Why Your Agent Can't Audit Itself](/blog/daemon-process-separation/) · [Native Tool Calls in the Audit Trail →](/blog/hooks-native-tool-audit/) +_Published 2026-05-17_ · **Series: Auditing AI Agents** · Part 2 of 3 · [← The audit boundary belongs outside the agent](/blog/daemon-process-separation/) --- This post walks through two things that landed together in `agent-receipts-daemon v0.10.0`: a unified receipt chain that captures both native Claude Code tool calls and MCP tool calls in a single sequence, and automatic secret redaction before any payload reaches the database. -Both were validated live. The receipts below are real. +The receipts below are real. --- @@ -22,7 +22,7 @@ Before v0.10.0, the picture looked like this: - The **MCP proxy** signed and stored receipts in its own `receipts.db` - The **Claude Code hook** forwarded events to the daemon, which stored them in the daemon's `receipts.db` -Two databases. Two chains. No way to correlate a native `Bash` call with the MCP tool call that triggered it, within the same audit trail. +Two databases, two chains, and no way to correlate a native `Bash` call with the MCP tool call that triggered it inside the same audit trail. ADR-0010 (Daemon Process Separation) was always meant to fix this: one daemon, one chain, all emitters forwarding events to it. v0.10.0 completes that picture. @@ -111,7 +111,7 @@ Both receipts include `parameters_disclosure.peer.*` — the OS-level credential } ``` -The daemon uses `LOCAL_PEERCRED` + `LOCAL_PEEREPID` on macOS to capture these at socket accept time. The emitter cannot forge them. `exe_path` is empty when the peer has already closed its end of the socket by the time the daemon resolves it: `LOCAL_PEEREPID` returns `ENOTCONN`, the pid is left at zero, and `proc_pidpath` is skipped. Short-lived emitters like the hook (one connection per frame, then exit) hit this path often; `uid`/`gid` still come through because `LOCAL_PEERCRED` snapshots them at connect time. +The daemon captures these at socket accept time, kernel-attested — the emitter cannot forge them. `uid` and `gid` come from `LOCAL_PEERCRED`, which snapshots them the moment the connection lands; they always come through. `pid` and `exe_path` are looked up just after via `LOCAL_PEEREPID` + `proc_pidpath`. Short-lived emitters that have already disconnected by then — the hook in particular, one connection per frame — leave those two empty (`LOCAL_PEEREPID` returns `ENOTCONN`). The kernel always wins on uid/gid; pid/exe_path is best-effort by design. --- @@ -121,7 +121,7 @@ The `parameters_disclosure` feature has a history. It started as `parameterPrevi That question drove two things: the rename from `parameterPreview` → `parameterDisclosure` (the field carries disclosure, not a safe preview), and the redaction system now in the daemon. The idea was ported from the plugin to the main project precisely because the daemon is the right place for it — it's the only process that sees the raw payload, making it the only place where redaction can run unconditionally, regardless of which emitter sent the event. -With `--parameter-disclosure` enabled, the daemon stores plaintext inputs and outputs in the receipt's `parameters_disclosure` field. Without redaction, this would be a liability: any secret that appears in a tool call argument or response would be persisted forever in a signed, immutable record. +With `--parameter-disclosure` enabled, the daemon stores plaintext inputs and outputs in the receipt's `parameters_disclosure` field. Without redaction, that's a liability — any secret in a tool call argument or response would end up in a signed, immutable record. ```mermaid sequenceDiagram @@ -196,6 +196,5 @@ The daemon is the only process that ever sees raw inputs — emitters are fire-a ## What's next - **Centralised storage** — the daemon today writes to a local SQLite file. The natural next step is a remote sink: ship receipts from multiple agents and machines to a shared store — Postgres, S3, or an OTel-compatible backend — so cross-agent audit trails can be correlated in one place -- **Breach-indicator flagging** (#436) — daemon-side pattern matching at emit time, writing indexed flags alongside receipts for efficient breach investigation queries without needing to decrypt stored payloads -- **Daemon config file** (#441) — replacing plist/unit file edits with a single `daemon.toml` - **`parameters_disclosure` encryption** (#280) — replacing plaintext disclosure with asymmetric ciphertext so only the forensic key holder can read stored inputs; the redaction layer ships first so secrets are never written even in the interim +- **Breach-indicator flagging** (#436) — daemon-side pattern matching at emit time, writing indexed flags alongside receipts for efficient breach investigation queries without needing to decrypt stored payloads From cb4d0916f8817da2efb52607a559c4d36b7acb97 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Sun, 17 May 2026 19:51:54 +1200 Subject: [PATCH 7/9] docs(blog): set redaction-demo publish date to 2026-05-21 --- site/src/content/docs/blog/unified-chain-redaction-demo.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/content/docs/blog/unified-chain-redaction-demo.mdx b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx index 0e0a976d..f66d5eaf 100644 --- a/site/src/content/docs/blog/unified-chain-redaction-demo.mdx +++ b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx @@ -5,7 +5,7 @@ description: "How the Claude Code hook and MCP proxy write to the same auditable import { Aside } from '@astrojs/starlight/components'; -_Published 2026-05-17_ · **Series: Auditing AI Agents** · Part 2 of 3 · [← The audit boundary belongs outside the agent](/blog/daemon-process-separation/) +_Published 2026-05-21_ · **Series: Auditing AI Agents** · Part 2 of 3 · [← The audit boundary belongs outside the agent](/blog/daemon-process-separation/) --- From 0c8e00f876953143938ef8d416a4c4a8ea46a1b7 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Sun, 17 May 2026 20:28:56 +1200 Subject: [PATCH 8/9] docs(blog): restore post 1 forward link to post 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When post 2 ships, post 1 should point at it. Adds the "Next →" entry to post 1's series header and restores the closing "the next post is the demo" line that was stripped when post 1 shipped without its companions. Bundling this with post 2 means a single merge does the right thing — no separate post-publish housekeeping PR. --- site/src/content/docs/blog/daemon-process-separation.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/src/content/docs/blog/daemon-process-separation.mdx b/site/src/content/docs/blog/daemon-process-separation.mdx index b13568b6..4e75b373 100644 --- a/site/src/content/docs/blog/daemon-process-separation.mdx +++ b/site/src/content/docs/blog/daemon-process-separation.mdx @@ -3,7 +3,7 @@ title: "The audit boundary belongs outside the agent" description: "The architectural problem with in-process receipt signing, and how daemon process separation restores the audit property." --- -_Published 2026-05-17_ · **Series: Auditing AI Agents** · Part 1 of 3 +_Published 2026-05-17_ · **Series: Auditing AI Agents** · Part 1 of 3 · [One Chain, Two Channels, Zero Secrets →](/blog/unified-chain-redaction-demo/) --- @@ -113,3 +113,5 @@ That signing key — `did:agent-receipts-daemon:local#k1` — lives only in the ## What comes next The daemon is the trust anchor for everything that follows: secret redaction before storage (already shipped), asymmetric payload encryption so only a forensic key holder can read stored inputs, centralised storage for cross-machine audit trails. All of it depends on having one trusted process that owns the keys and the chain. + +The next post is the demo — the unified chain and secret redaction working end to end, on real receipts. From ee8bc2cfb936c78231c9e009f1cdf8199d7226b9 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Sun, 17 May 2026 20:30:43 +1200 Subject: [PATCH 9/9] docs(blog): tighten trust-model framing on redaction-demo post MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Copilot feedback on PR #442 — two spots claimed the daemon is "the only process that sees raw inputs / sees the raw payload." That's inaccurate: emitters (mcp-proxy, hook) necessarily see the raw payload to forward it. The accurate claim is that the daemon is the only place where receipts are persisted and signed, and the only convergence point all emitters funnel through. - Redaction section: reframe to "every emitter funnels through the same daemon socket, so redaction applied there runs unconditionally." - What this enables: "the daemon is the only process that persists receipts and holds the signing key — emitters are fire-and-forget, no local storage." Same point as before — trust boundary is structural, daemon is the chokepoint — but the wording no longer overclaims about who sees what. --- site/src/content/docs/blog/unified-chain-redaction-demo.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/content/docs/blog/unified-chain-redaction-demo.mdx b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx index f66d5eaf..ef3f0c5d 100644 --- a/site/src/content/docs/blog/unified-chain-redaction-demo.mdx +++ b/site/src/content/docs/blog/unified-chain-redaction-demo.mdx @@ -119,7 +119,7 @@ The daemon captures these at socket accept time, kernel-attested — the emitter The `parameters_disclosure` feature has a history. It started as `parameterPreview` inside the [OpenClaw plugin](https://github.com/agent-receipts/openclaw) — a way to store a sanitised preview of tool call parameters alongside the hash, so auditors could see *what* was called without needing to re-run the tool ([how the plugin works](/blog/openclaw-plugin-deep-dive/)). The [first end-to-end trial](https://jongerius.solutions/post/agent-receipts-openclaw-v0-trial/) put it to work in anger, and an obvious question surfaced almost immediately: what happens when a tool call argument contains an API key or a password? The preview stores it. Forever. In a signed record. -That question drove two things: the rename from `parameterPreview` → `parameterDisclosure` (the field carries disclosure, not a safe preview), and the redaction system now in the daemon. The idea was ported from the plugin to the main project precisely because the daemon is the right place for it — it's the only process that sees the raw payload, making it the only place where redaction can run unconditionally, regardless of which emitter sent the event. +That question drove two things: the rename from `parameterPreview` → `parameterDisclosure` (the field carries disclosure, not a safe preview), and the redaction system now in the daemon. The idea was ported from the plugin to the main project precisely because the daemon is the right place for it — every emitter funnels through the same daemon socket, so redaction applied there runs unconditionally, regardless of which emitter sent the event. With `--parameter-disclosure` enabled, the daemon stores plaintext inputs and outputs in the receipt's `parameters_disclosure` field. Without redaction, that's a liability — any secret in a tool call argument or response would end up in a signed, immutable record. @@ -189,7 +189,7 @@ Every native tool call and every MCP tool call from a Claude Code session now la - Verify any receipt's signature and hash chain offline with `agent-receipts verify` - Inspect recent receipts with `agent-receipts list --json | jq ...` -The daemon is the only process that ever sees raw inputs — emitters are fire-and-forget. If the daemon is down, the hook logs a visible non-blocking error and the tool call completes normally; no receipt is written for that call and the gap is visible in the chain on restart. +The daemon is the only process that persists receipts and holds the signing key — emitters are fire-and-forget, no local storage. If the daemon is down, the hook logs a visible non-blocking error and the tool call completes normally; no receipt is written for that call and the gap is visible in the chain on restart. ---