Docs menuEvidence

Docs The loop

Evidence

Show that someone who does not trust you can check the record without Runback.

Every plan

Evidence. Every run is kept as a record that anyone can re-check: each event is hash-chained to the one before it, and the record is signed. Change anything and the check fails at the place it changed.

The tamper test
Loading /sample-cassette.json…

The record above is /sample-cassette.json, the same file the command below downloads. It is a loan-approval run, not the refund run: a fixed record built by the code that builds every customer export, so it is identical on every deploy. Each check goes to POST /api/audit/verify, which runs the same verification as the CLI and stores nothing.

How to: export and verify a record

  1. Download the record

    Open the run in Runs. In the run's own Audit log section, select Download. You get runback-audit-RUN_ID.json, with the run's id in place of RUN_ID.

  2. Verify it, offline

    Anyone can run this, with no Runback account and nothing sent anywhere:

    terminalShell
    npx @runback/verify runback-audit-RUN_ID.json
    Expected output

    runback.audit/v2 — VALID

    ✓ schema

    ✓ summary

    ✓ chain

    ✓ digest

    ✓ cassette

    ✓ signature — Ed25519 — verified against Runback's published key

    The command exits 0. A changed record prints INVALID, marks the failed check ✗ and exits 1.

  3. Export the evidence pack for a period

    For an auditor, open Evidence, choose the Reporting period, then select Download report. From Enterprise

Check it on your own machine

Verify sampleShell
#!/usr/bin/env bash
# Verify the published sample record on your own machine. Nothing is sent anywhere.
set -euo pipefail

curl -fsSLO https://runback.dev/sample-cassette.json
npx @runback/verify sample-cassette.json
# Exit 0 = VALID, 1 = INVALID (a check failed), 2 = UNVERIFIED (intact, signer not confirmed).

The command has its own words for a result. This is what each one means here:

The command printsExit codeIn Runback's words
VALID0Intact and Signed: every check passed, and the signature checks against Runback's published key (or the HMAC key you passed with --key).
UNVERIFIED2Intact, signer not confirmed: the record is unchanged, but it is signed by another key, such as a self-hosted deployment's, or not signed at all. Not tampering, and not a pass.
INVALID1Changed: a check failed. The ✗ marks which one.

What is checked

Anyone can author a record that passes the first five checks, because the chain algorithm is public; only the signature says who made it. The exact algorithm, canonical JSON and the signature formats are in the record spec.

The first five say the record is intact. The sixth says who signed it.

CheckThe command printsWhat passing it shows
FormatschemaThe record declares a format this verifier knows.
SummarysummaryThe run id and outcome in the summary agree with the signed events, so a summary cannot misstate the run however intact the chain is.
Hash chainchainEvery event's recomputed hash matches the one stored beside it.
Content digestdigestThe last hash in the chain matches the digest in the record's manifest.
Recording digestcassetteThe recorded model, tool and environment results still match the digest taken when they were recorded.
SignaturesignatureWho signed the record. Signed by Runback's published key is a pass; signed by another key, or not signed, leaves the record intact but its signer unconfirmed.

Signatures

A deployment signs records with Ed25519 when it sets AUDIT_ED25519_PRIVATE_KEY; without it, it falls back to the legacy HMAC-SHA256 over AUDIT_SIGNING_KEY, which only the holder of the secret can check. What the command reports depends on the key:

The record is signed withThe command's JSONIt printsIn Runback's words
Runback's published Ed25519 keyvalidVALIDSigned
Another Ed25519 key, such as a self-hosted deployment'svalid-unpinnedUNVERIFIEDIntact, signer not confirmed
HMAC, checked with the secret passed as --keyvalidVALIDSigned by the key you passed
HMAC, without --keyno-keyUNVERIFIEDIntact, signer not confirmed
NothingunsignedUNVERIFIEDIntact, signer not confirmed

Each event's hash covers the one before it, so the record is a chain, not a Merkle tree:

Hash chainText
h(i) = SHA-256(h(i-1) + canonical(event i))

The rest of Evidence

In the app, under EvidenceWhat it holds
EvidenceThe period's evidence pack, signed
FrameworksThe controls each regulation maps to
Audit logEvery entry, and the checkpoints that sign them
ActivityAdmin changes and sign-ins in the workspace

Outside the app, the verifier page checks any exported record, and Enterprise covers regulator exports and auditor access.

Last updated 2 Nov 2026.