docs/book/src/security/tool-receipts.md
Tool receipts are cryptographic proofs that a tool actually ran. Every tool invocation — approved, blocked, or auto-approved — produces an HMAC-SHA256 digest over the call and its result. The digest is appended to the tool-result text and passed back to the model as part of the conversation.
The practical outcome: the model cannot claim to have run a tool it didn't run, and it cannot fabricate a tool result. Both produce receipt mismatches the runtime detects.
An LLM is a string generator. By default, nothing prevents it from narrating a tool call it never made ("I ran git log and the latest commit is…"), or inventing a result for a tool call ("The weather API says 72°F" — when the call timed out). For an agent with autonomy, this is more than a correctness issue — it's a deniability issue.
Tool receipts close that gap with the cheapest possible construct: a symmetric MAC with an ephemeral per-session key.
Based on: Basu, A. (2026). "Tool Receipts, Not Zero-Knowledge Proofs: Practical Hallucination Detection for AI Agents." arXiv:2603.10060.
receipt = HMAC-SHA256(key, tool_name || args || result || timestamp)
[receipt: zc-receipt-<timestamp>-<base64url-digest>]
The model sees every receipt in its conversation history. It can echo them in text it produces to the user. But it cannot produce a new valid receipt — the HMAC requires the session key, which the model doesn't have.
zc-receipt-1774608496-gzpEBuUIRYX1vd4fQl4oYkqhq4-GnoJDStmlYzvQiWA
^ epoch seconds ^ base64url(HMAC-SHA256 digest)
The zc-receipt- prefix exists so the leak detector doesn't redact them (receipts are safe to surface; they contain no secret material).
| Scenario | Without receipts | With receipts |
|---|---|---|
| Model claims it ran a tool, didn't | Undetectable | No receipt — fabrication visible |
| Model fabricates a result for a real call | Undetectable | HMAC mismatches on verification |
| Model denies a call it did make | Unverifiable | Receipt in log proves it |
| Model fabricates a plausible receipt string | Plausible | HMAC verification fails |
RUST_LOG=zeroclaw::agent=debug zeroclaw daemon
Produces:
DEBUG Tool receipt generated tool=shell receipt=zc-receipt-1774604899-fVRG...
If [agent.tool_receipts] show_in_response = true, the reply includes a trailing block:
Here's the weather in Istanbul: 16°C, sunny.
---
Tool receipts:
weather: zc-receipt-1774608496-gzpEBuUIRYX1vd4fQl4oYkqhq4-GnoJDStmlYzvQiWA
Because the model sees receipts in its context, it may echo them when describing tool results. The leak detector is configured to pass zc-receipt-* tokens through unmodified so this echoing works. If both the runtime and the model include a receipts block, the user sees two — strip one via channel-specific formatting rules.
[agent.tool_receipts]
enabled = true
show_in_response = false # append trailing "Tool receipts:" block
inject_system_prompt = true # instruct the model to echo receipts verbatim
hmac + sha2 from the Rust ecosystem.| Feature | Status |
|---|---|
| HMAC generation per call | Shipped |
| Receipt appended to tool result | Shipped |
| Debug log of receipts | Shipped |
show_in_response | Shipped |
| System-prompt instruction to echo receipts | In flight |
| Persistent audit database of receipts | Planned |
| Cross-session receipt verification | Not planned (see ephemeral-key design) |