Coco

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 ledger
2026-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 credential

An 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 downstream

The 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 report

The 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 verify
Chain 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.

On this page