Every Runback audit export is a hash-chained JSON record. Click Verify chain below and it is checked in front of you — nothing to install, no account. Prefer to keep it off our machines entirely? Run npx @runback/verify on your own, or paste a record below, which sends it to us to check and keeps nothing. If the chain verifies, the record is internally consistent; if the signature also verifies against our published key, it's exactly what the agent produced. Either check failing tells you precisely which one broke.
Records are signed with Ed25519 wherever a keypair is configured — asymmetric, so a third party can check it offline against our published public key, no secret changes hands. Without a keypair, deployments fall back to HMAC-SHA256: the hash chain is still fully checkable by anyone, but the signature itself only by whoever holds the key. The verifier tells you which one you're looking at.
This is what “tamper-evident” actually means: the proof is in the chain, not in trusting the vendor.
The demo record below is a real audit export from the loan-approval incident walked through in How it works. Prefer your own machine? Download the same record and run npx @runback/verify sample-cassette.json. Click Verify chain and watch each entry's hash check against the one before it, then the signature check establish who produced it.
{
"$schema": "runback.audit/v2",
"manifest": {
"run_id": "run_a3f1b90c",
"generated_at": "2026-06-14T02:47:13.950Z",
"event_count": 6,
"algorithm": "sha256-chain",
"content_digest": "2e91e3f5fc451d2c9841d9e3b6c68d78a9b2fca00fdff98989512704180a9189",
"replay": {
"cassette_digest": "4376f4529252da46ea7da2f5a0afb59f5cb3511b13459ef8f67cf848f7a02cf8",
"entry_count": 4,
"algorithm": "oracle-chain/sha256",
"note": "The run's oracle stream — every model response and tool output, hash-chained. Re-execute the agent against `events` with @runback/replay and the cassette digest must reproduce. This is proof the recording IS the deterministic input stream of the run — an audit you can re-run, not just read."
},
"signed": true,
"signature": {
"alg": "Ed25519",
"value": "d6ab935b3ec9d96883ccb689d5589a50c985fa7109f1e321724a2623d417d7bce7f01a8707a5e11848d7a97c759370deee37297d8f6f3392d29f0d9b53e2e70d",
"pubkey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEArDPmR+Gvu1HawnSvM6xjZao7BVPPQ80Qq3sa1hAYvDU=\n-----END PUBLIC KEY-----\n"
},
"verify": "Recompute the event chain h_i = SHA-256(h_{i-1} + canonical(event_i)) over `events` → must equal content_digest. Recompute the oracle-stream digest from `events` → must equal replay.cassette_digest. The signature covers `${content_digest}:${cassette_digest}`. Ed25519: verify with the public key embedded in manifest.signature.pubkey (no server key needed — fully offline-verifiable). HMAC-SHA256 (legacy): requires AUDIT_SIGNING_KEY. POST this record to /api/audit/verify.",
"spec_url": "https://runback.dev/spec",
"verifier_url": "https://runback.dev/verify"
},
"run": {
"run_id": "run_a3f1b90c",
"name": "loan-approval-agent",
"status": "error",
"input": "Applicant #4471 requests a $12,000 personal loan.",
"output": null,
"error": {
"name": "PolicyBlock",
"message": "issue_approval blocked — manual-review-required"
},
"metadata": {
"demo": true
},
"step_count": 2,
"total_tokens": 1030,
"started_at": "2026-06-14T02:47:11.000Z",
"ended_at": "2026-06-14T02:47:13.950Z",
"actor_type": null,
"actor_id": null
},
"events": [
{
"schema_version": 1,
"run_id": "run_a3f1b90c",
"span_id": "r",
"parent_span_id": null,
"seq": 0,
"ts_start": "2026-06-14T02:47:11.000Z",
"ts_end": null,
"type": "run",
"phase": "start",
"name": "loan-approval-agent",
"input": "Applicant #4471 requests a $12,000 personal loan.",
"output": null,
"status": "running",
"error": null,
"metadata": {
"demo": true
},
"_hash": "07bda052e5ce563160c1730ddefb62bace30cbeb871190097ee081d2a8570094"
},
{
"schema_version": 1,
"run_id": "run_a3f1b90c",
"span_id": "l1",
"parent_span_id": "r",
"seq": 1,
"ts_start": "2026-06-14T02:47:11.000Z",
"ts_end": "2026-06-14T02:47:12.400Z",
"type": "llm",
"model": {
"provider": "openai",
"model_id": "gpt-4o"
},
"request": {
"system": "You are a careful lending agent. Follow policy.",
"messages": [
{
"role": "user",
"content": "Applicant #4471 requests a $12,000 personal loan."
}
],
"tools": [
{
"name": "check_credit_file",
"description": "Pull the applicant's credit file.",
"parameters": {
"type": "object",
"properties": {
"applicant_id": {
"type": "number"
}
}
}
}
],
"params": {
"temperature": 0
}
},
"response": {
"text": "Checking the credit file before deciding.",
"reasoning": null,
"finish_reason": "tool-call",
"tool_calls": [
{
"tool_call_id": "t1",
"tool_name": "check_credit_file",
"input": {
"applicant_id": 4471
}
}
]
},
"usage": {
"input_tokens": 428,
"output_tokens": 184,
"total_tokens": 612
},
"latency_ms": 1400,
"error": null,
"_hash": "8f985ca54090e255926fb55830ecf4ab4dc4f9f32b598e04f50e34aaf09745fb"
},
{
"schema_version": 1,
"run_id": "run_a3f1b90c",
"span_id": "t1",
"parent_span_id": "l1",
"seq": 2,
"ts_start": "2026-06-14T02:47:12.400Z",
"ts_end": "2026-06-14T02:47:12.618Z",
"type": "tool",
"tool_name": "check_credit_file",
"tool_call_id": "t1",
"input": {
"applicant_id": 4471
},
"output": {
"score": 684,
"band": "near-prime",
"disputes_open": 1
},
"latency_ms": 218,
"error": null,
"_hash": "0fad198fd6469eb12aaa162d8acb5a0d9002f5eaa88806f4a4b6a79383b999bf"
},
{
"schema_version": 1,
"run_id": "run_a3f1b90c",
"span_id": "l2",
"parent_span_id": "r",
"seq": 3,
"ts_start": "2026-06-14T02:47:12.618Z",
"ts_end": "2026-06-14T02:47:13.900Z",
"type": "llm",
"model": {
"provider": "openai",
"model_id": "gpt-4o"
},
"request": {
"system": "You are a careful lending agent. Follow policy.",
"messages": [
{
"role": "user",
"content": "Applicant #4471 requests a $12,000 personal loan."
},
{
"role": "tool",
"content": "{\"score\":684,\"band\":\"near-prime\",\"disputes_open\":1}"
}
],
"tools": [
{
"name": "issue_approval",
"description": "Approve the loan.",
"parameters": {
"type": "object",
"properties": {
"amount": {
"type": "number"
}
}
}
}
],
"params": {
"temperature": 0
}
},
"response": {
"text": "Credit is sufficient — approving.",
"reasoning": null,
"finish_reason": "tool-call",
"tool_calls": [
{
"tool_call_id": "t2",
"tool_name": "issue_approval",
"input": {
"applicant_id": 4471,
"amount": 12000
}
}
]
},
"usage": {
"input_tokens": 286,
"output_tokens": 132,
"total_tokens": 418
},
"latency_ms": 1282,
"error": null,
"_hash": "76dbdec70aef43d145f8668c6401248be453f78655bba38957c33a36bf83cfe8"
},
{
"schema_version": 1,
"run_id": "run_a3f1b90c",
"span_id": "t2",
"parent_span_id": "l2",
"seq": 4,
"ts_start": "2026-06-14T02:47:13.900Z",
"ts_end": "2026-06-14T02:47:13.950Z",
"type": "tool",
"tool_name": "issue_approval",
"tool_call_id": "t2",
"input": {
"applicant_id": 4471,
"amount": 12000
},
"output": null,
"latency_ms": 50,
"error": {
"name": "PolicyBlock",
"message": "Blocked: an open dispute requires manual review before approval."
},
"policy_block": {
"rule": "manual-review-required",
"detail": "when credit_file.disputes_open gt 0 → must call escalate_to_human"
},
"policy_evaluated": {
"passed": false
},
"_hash": "bad224a034ed84cd8a216847e2f37084e5b7428f32b0868737faffcc007408a7"
},
{
"schema_version": 1,
"run_id": "run_a3f1b90c",
"span_id": "re",
"parent_span_id": null,
"seq": 5,
"ts_start": "2026-06-14T02:47:13.950Z",
"ts_end": "2026-06-14T02:47:13.950Z",
"type": "run",
"phase": "end",
"name": "loan-approval-agent",
"input": null,
"output": null,
"status": "error",
"error": {
"name": "PolicyBlock",
"message": "issue_approval blocked — manual-review-required"
},
"metadata": {},
"_hash": "2e91e3f5fc451d2c9841d9e3b6c68d78a9b2fca00fdff98989512704180a9189"
}
]
}The ledger is append-only. Remove any entry and the chain breaks at exactly that point. A gap in the sequence is detectable, not concealable. (Enforced by pg_advisory_xact_lock per org.)
Change any field in a past entry — decision text, a tool output, a timestamp — and its SHA-256 hash no longer matches what the next entry was sealed against. The failure cascades forward: every downstream hash is invalidated.
A record can't be re-pointed at a different replay after the fact — the signature covers the decision log and the replay data together, as one sealed unit, not two things trusted separately. (Signed payload: content_digest:cassette_digest.)
The ledger's tree structure is built so a piece of one entry can never be mistaken for a piece of another, even by someone trying to forge it. (Domain-separated Merkle tree: leaf nodes prefixed "leaf:", internal nodes "node:", preventing second-preimage attacks.)
When a signing key is configured, a missing or invalid signature fails verification — it does not pass. DB write access alone cannot forge a valid checkpoint. The system defaults to rejection, not trust.
The same verification algorithm ships as a standalone CLI — install anywhere, pipe in any export, get a machine-readable verdict. No account, no network call. It auto-detects the shape you paste in — audit record, sealed AI narrative, or security-tool finding — each verified against its own chain and signature.
npm install -g @runback/verify
runback-verify audit-export.json
cat audit-export.json | runback-verify --stdin
runback-verify audit-export.json --json
{
"valid": true,
"verdict": "valid",
"integrity": true,
"checks": {
"schema": true,
"chain": true,
"digest": true,
"cassette": true,
"signature": "valid",
"signature_alg": "Ed25519"
}
}Exit codes: 0 valid · 2 integrity holds but the origin is unproven · 1 a check failed. Gate CI on 0, not on “not 1”.
The verifier is published on npm as @runback/verify — run it with npx @runback/verify audit.json. The algorithm is the standard; anyone can implement it.
The free Community edition captures every agent run with a signed, verifiable audit record — self-hosted, in your own cloud, forever.
What EU AI Act Art. 12 and APRA CPS 230 ask for, in practice — see the framework mapping →