Getting started
Two paths. The SDK path records what the agent's own process reports: the quickest way to a receipt, and every field in it is the agent's word (claimed). The gateway path puts a process the agent does not control between it and the tools, so a call can be denied before it runs and a stranger can accept the record. Start with the SDK to see a receipt today; move a tool to the gateway when its call moves money or touches production. Either path runs in observe mode first, "mode": "observe" in the config, recording what the policy would deny without blocking anything, until the policy is right.
Pick your stack
Install the package, make a key and a config once, then add the lines for your framework: Claude Code, the Claude Agent SDK, the OpenAI Agents SDK, LangChain, Vercel AI, OpenClaw, DeepSeek Harness, Hermes, Google ADK, or plain Python. Each produces a receipt in receipts/ that the browser verifier checks with the .pub file.
npm install @agent-custody/receipts && npx agent-custody keygen --dir keys --name app// .claude/settings.json: one hook command records every tool call and can deny one before it runs
{ "hooks": {
"PreToolUse": [{ "matcher": "Bash|Write|Edit|mcp__.*", "hooks": [{ "type": "command", "command": "agent-custody hook --config /abs/path/sdk.json" }] }],
"PostToolUse": [{ "matcher": "Bash|Write|Edit|mcp__.*", "hooks": [{ "type": "command", "command": "agent-custody hook --config /abs/path/sdk.json" }] }]
} }import { query } from "@anthropic-ai/claude-agent-sdk";
import { loadSdkConfig } from "@agent-custody/receipts";
import { claudeAgentHooks, createSdkIssuer } from "@agent-custody/receipts/sdk/claude";
const issuer = createSdkIssuer(loadSdkConfig("./sdk.json"));
for await (const msg of query({ prompt: "Refund the customer", options: { hooks: claudeAgentHooks(issuer) } })) { /* … */ }import { Agent } from "@openai/agents";
import { loadSdkConfig, createSdkIssuer } from "@agent-custody/receipts";
import { wrapTools } from "@agent-custody/receipts/sdk/openai-agents";
const issuer = createSdkIssuer(loadSdkConfig("./sdk.json"));
const agent = new Agent({ name: "billing", tools: wrapTools(issuer, [refundTool, lookupTool]) }); // a denied call never runs; the model sees whyimport { tool } from "@langchain/core/tools";
import { loadSdkConfig, createSdkIssuer } from "@agent-custody/receipts";
import { receiptCallbacks } from "@agent-custody/receipts/sdk/langchain";
const issuer = createSdkIssuer(loadSdkConfig("./sdk.json"));
await refund.invoke({ customer_id, amount }, receiptCallbacks(issuer)); // record what happened
const enforced = tool(issuer.wrap("stripe.refund", fn), { name: "stripe.refund", schema }); // or deny before it runsimport { generateText } from "ai";
import { loadSdkConfig, createSdkIssuer } from "@agent-custody/receipts";
import { wrapTools } from "@agent-custody/receipts/sdk/vercel-ai";
const issuer = createSdkIssuer(loadSdkConfig("./sdk.json"));
const result = await generateText({ model, prompt, tools: wrapTools(issuer, tools) });// an OpenClaw plugin entry: every tool call the agent makes, a denied one blocked before it runs
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-sdk";
import { createSdkIssuer, loadSdkConfig } from "@agent-custody/receipts";
import { registerOpenClaw } from "@agent-custody/receipts/sdk/openclaw";
export default definePluginEntry({ id: "agent-custody", name: "agent-custody", description: "A receipt for every tool call",
register(api) { registerOpenClaw(api, createSdkIssuer(loadSdkConfig("./sdk.json"))); } });# the module is the plugin: list it in cordis.yml (dsh web --patch ./cordis.yml) with the SDK config as its setting
- insert:
- id: agent-custody
name: "@agent-custody/receipts/sdk/deepseek-harness"
config:
config: /abs/path/sdk.json# ~/.hermes/plugins/agent-custody/plugin.yaml, beside an __init__.py that reads: from agent_custody.hermes import register
name: agent-custody
version: 1.0.0
provides_hooks: [pre_tool_call, post_tool_call]
config_schema:
sidecar_url: { type: str, default: "http://127.0.0.1:8791" }
# then: pip install agent-custody && hermes plugins enable agent-custody# pip install "agent-custody[adk]"; the sidecar is `npx agent-custody serve --config sdk.json`
from google.adk.agents import LlmAgent
from agent_custody import Client
from agent_custody.adk import adk_callbacks
agent = LlmAgent(name="billing", model="gemini-2.5-flash", tools=[refund, lookup], **adk_callbacks(Client())) # a denied call is skipped; the model sees the receipt# pip install agent-custody; the sidecar is `npx agent-custody sidecar`
from agent_custody import Client, PolicyDeniedError
client = Client("http://127.0.0.1:8791")
refund = client.wrap("stripe.refund", lambda args: stripe.refund(**args))
refund({"amount": 5000}) # decide, run, record; raises PolicyDeniedError on denyThen verify the receipt in the shell, or drop it on /verify:
npx agent-custody verify receipts/<id>.json --issuer-key keys/app.pub --log log.jsonlEvery adapter is on the SDK page with what it enforces and what it only records. To make a receipt evidence a customer or auditor accepts, put the gateway in front of the tool and log to a log run by someone else: the receipt then carries a signed grant, a policy decision made outside the agent, and a tree head signed by a key you do not hold.
The whole loop, in one file
Node 22 or later. Both packages ship compiled JavaScript with type declarations.
npm install @agent-custody/receipts @agent-custody/state1. Give the agent's process a signing key and a config. The key signs every receipt. The config says where receipts and the log go.
npx agent-custody keygen --dir keys --name app{ "agentId": "support-bot", "identity": { "keyFile": "keys/app.key" }, "receiptsDir": "receipts", "logFile": "log.jsonl" }2. Record a tool call as a receipt. Wrap the functions the agent calls, or record a call explicitly. Either way a signed receipt lands in receipts/ and a leaf in the Merkle log.
import { createSdkIssuer, loadSdkConfig } from "@agent-custody/receipts";
const issuer = createSdkIssuer(loadSdkConfig("./sdk.json"));
const lookup = issuer.wrap("crm.lookup", async (args: { id: string }) => crm.lookup(args)); // records every call
const customer = await lookup({ id: "acct:42" });
const bundle = issuer.record({ tool: "crm.lookup", args: { id: "acct:42" } }, { status: "executed", result: customer }); // or record one by hand and keep the bundle3. Verify it, offline. Anyone with the public key and a copy of the log can check the signature, the statement, and the inclusion proof. No access to the agent or the issuer needed.
import { loadPublicKey, verifyBundle } from "@agent-custody/receipts";
verifyBundle(bundle, { issuerKeys: [loadPublicKey("keys/app.pub")], principalKeys: [], logFile: "log.jsonl" }).ok; // truenpx agent-custody verify receipts/<id>.json --issuer-key keys/app.pub --log log.jsonl # the same check from the shell4. Record what the agent now believes, citing the receipt. The ledger is bitemporal: it knows when a fact was true and when the agent learned it. A belief points back at the receipt that produced it.
import { receiptIdOf } from "@agent-custody/receipts";
import { Ledger } from "@agent-custody/state";
const ledger = new Ledger("./ledger.jsonl"); // or "./ledger.sqlite", or "postgres://…"
const belief = await ledger.assert({ subject: "acct:42", predicate: "plan", value: customer.plan, space: "org", actor: "support-bot", source: { receiptId: receiptIdOf(bundle) } });5. When a belief is wrong, undo it without losing the record. Retract removes it from the present, keeps it visible to questions about the past, and restores whatever it had superseded. The receipt id says exactly which call produced the bad belief.
await ledger.retract({ factId: belief.fact.factId, actor: "user:admin", reason: "CRM lookup returned a stale plan" });
await ledger.asOf({ subject: "acct:42" }); // [] now
await ledger.asOf({ subject: "acct:42", validAt: earlier, txAt: earlier }); // still shows what was believed then
await ledger.history(belief.fact.factId); // assert, retract, with actors and reasonsThis whole loop is one runnable file, packages/state/examples/02-receipt-to-belief.ts, executed by the test suite.
Where to go next
- Enforce instead of record: put the gateway between the agent and its MCP tools. The gateway
- Hook an existing framework, or another language. The interceptor SDK
- Write policies, for tools and for memory, with tested examples. Policies, and what a verified receipt proves: Verification
- Put beliefs in the ledger. State