Reading the ledger
What the rows hold, how to filter them, and what the chain does and does not prove.
Reading the ledger
One row per decision, allows included.
coco ledger2026-08-24T05:57:45Z BLOCK Bash rm -rf /
#1 baseline.destructive_command/no_destructive_operation
The command is a destructive operation with no safe reading in a
governed workflow
2026-08-24T05:57:45Z ESCALATE Read /tmp/.env
#2 baseline.credential_access/credential_read_needs_a_person
The agent is reading a file that holds a credentialAn ALLOW prints one line. Anything stopped prints its receipt id, the contract, the specific check inside it and the reason, because those are the rows someone is about to ask about.
Filtering
coco ledger --verdict BLOCK # only what was stopped
coco ledger --limit 200 # further back than the default 40
coco ledger --limit 200 --json # for anything downstreamThe JSON carries every field on the receipt, including the hashes and the session counters as they stood before the call.
Over HTTP the same rows come back as JSON.
curl -s "$GATE/receipts?limit=50" -H "Authorization: Bearer $COCO_API_KEY"The catch report
coco reportThe report reads the last thousand decisions, takes everything that would not have passed enforce mode, and groups it by the check that stopped it with up to three examples from your own traffic.
That grouping is the whole value. A rule firing forty times is a drafting problem you can see in one line, and a rule that fired twice on things that should not have happened is your argument for enforcement.
Nothing caught means one of two things and the report says so. Either the contracts are too loose or the work was clean, and only you can tell which.
Verifying the chain
coco verifyChain verified. 3 receipts, unbroken.Three things are checked. The chain itself, the content hash of each receipt, and whether the queryable columns still say what the hashed receipt says. Rewriting a column is the cheapest way to alter a record, so it is checked rather than assumed.
A break names the first row that does not verify.
What the chain proves and does not
The rows are tamper-evident on one machine. A rewritten record shows up as a broken chain rather than a quiet edit, and the chain is written inside a transaction so two parallel calls produce two links instead of a fork.
The receipts are not cryptographically signed. They would not survive an operator with write access and patience. Signing and external anchoring are the next step, and calling the current state signed would be wrong.
There are two holes worth knowing before an auditor finds them. A hook that timed
out leaves no row at all, because the receipt is written by the check itself. And on
the SDK road a verdict the client produced because the gate did not answer is marked
unreached, and no receipt exists for it, because the request never arrived.
Both holes are visible as gaps rather than as wrong answers, and both are as true in
observe mode as in enforce. Finding them is a reconciliation, not a mystery. Your
harness knows every check it asked for, so count its calls against the gate's rows
for the same session, and log every verdict that came back unreached, because each
one is a row that does not exist. A gap between the two counts is the hole, located
to a session.
Where it lives
Locally the ledger is a SQLite file at ~/.coco/ledger.db, and there is no daemon
and no service to keep running. In a container it is at /data/ledger.db, which is
the only state on the machine worth a backup. Mount that volume, or the hash chain
restarts every time the container does.