Flight Recorder Recipes for Audits and Incident Reviews
The flight recorder needs no setup. It starts writing the moment Tokening 0.1.2 or later opens your database. What it does need is a few habits around it. Here are the ones we’d start with.
Every command below was run against the database from our case studies, on Tokening 0.1.4, and the output shown is copied from the recorded runs. Paths are examples. All flight commands open the database read-only, so they’re safe next to a running server.
Save the head from a job you already run
A hash chain catches edits and deletions by itself. A full rebuild of the chain needs a saved head to catch it, as the edited log case study shows. You don’t need a new scheduler for this. Add one line to something that already runs: the nightly backup, a deploy script, the end-of-day report.
tokening flight head --db /var/lib/tokening/tokening.db >> /mnt/backup-host/tokening-heads.txt
It prints one line:
20:adff53dd54fbae58a0df9c57fa306b995135b864d228365fc9bd2ca2511fc276
What matters is where the line ends up. It has to be somewhere the person who controls the database can’t quietly rewrite: another machine, a ticket, a commit in a repo they can’t force-push, a message to a colleague. A file next to the database proves nothing.
Later, any saved line can be checked:
$ tokening flight verify --db tokening.db --head 20:adff53dd54fbae58a0df9c57fa306b995135b864d228365fc9bd2ca2511fc276
Intact: 20 entries, 1 to 20.
Head: 20:adff53dd54fbae58a0df9c57fa306b995135b864d228365fc9bd2ca2511fc276
The saved head is in the chain.
Verify in CI or before an audit
verify exits 0 when the log is intact and 1 when it isn’t, so it drops into any script that stops on failure.
head=$(tail -n 1 /mnt/backup-host/tokening-heads.txt)
if [ -z "$head" ]; then echo "No saved head found" >&2; exit 1; fi
tokening flight verify --db /var/lib/tokening/tokening.db --head "$head"
The empty check can stay, since it gives a clearer message in your own logs. Tokening 0.1.4 also refuses an empty head by itself. In 0.1.3 an empty --head "" was treated as no head at all: verify checked the chain, skipped the head and still exited 0. Now it stops with exit 2:
$ tokening flight verify --db tokening.db --head ""
--head is empty. Give SEQ:HASH as printed by tokening flight head, or leave --head out. If it came from a file of saved heads, that file is empty or its last line is blank.
With a saved head, a passing run ends with “The saved head is in the chain.”
On the case study database it said “Intact: 20 entries, 1 to 20.” with exit 0. On a copy where one decision had been edited by hand:
$ tokening flight verify --db tampered.db
Broken at entry 3: entry 3 does not match its own hash: its contents were changed after it was written.
Entries 1 to 2 verify. Last good head: 2:496294b10a0b0e81d364b9298ce439ecffe7b60b2266b5d22a7b3c984291d5b8
Exit 1. Run it on a copy of the production database if your CI can’t reach the real file. A check of a backup is still a check of everything up to that backup.
Audits and compliance reviews tend to ask whether a record of automated actions could have been changed after the fact. This gives you a concrete answer for everything that went through Tokening, and the saved heads show how far back that answer goes.
Export one agent and a time window
$ tokening flight export --db tokening.db --agent editor-bot --since 2026-10-31T10:40:11Z --until 2026-10-31T10:40:14Z --output editor-bot.jsonl
Exported 10 entries.
--since is inclusive and --until isn’t. A UTC date such as 2026-10-31 works too. --grant narrows it to one grant. The output file must not exist yet; export never overwrites.
Whoever receives the file checks it with --partial, because a filtered export skips entries on purpose:
$ tokening flight verify --file editor-bot.jsonl --partial
Intact as a partial export: 10 entries from 9 to 18, every hash checked, links checked where entries are consecutive.
The reviewer export case study walks through what a reviewer sees, including an edited copy being caught.
Replay one grant after an incident
When something went wrong, start from the grant. One agent may hold several grants, and the grant is what the operator signed off on.
$ tokening flight replay --db tokening.db --grant g_bc3cbaec237464d10e22cbe6b3615f88
#9 2026-10-31T10:40:12.698Z grant g_bc3cbaec237464d10e22cbe6b3615f88 issued to editor-bot by ops-maria: cms.edit-post-text on update_post, 2 uses, expires 2026-10-31T10:55:12.698Z, bound id, reason "fix typo in post 3 title"
#10 2026-10-31T10:40:12.705Z request g_bc3cbaec237464d10e22cbe6b3615f88 editor-bot asked for update_post on cms with arguments id, title (digest 87053af89cbe)
#11 2026-10-31T10:40:12.705Z decision g_bc3cbaec237464d10e22cbe6b3615f88 allowed: Call fits this grant. One use was consumed before forwarding. 1 use left.
#12 2026-10-31T10:40:12.716Z result g_bc3cbaec237464d10e22cbe6b3615f88 upstream answered HTTP 200 in 7 ms
#13 2026-10-31T10:40:12.719Z request g_bc3cbaec237464d10e22cbe6b3615f88 editor-bot asked for update_post on cms with arguments id, status, title (digest ee3b7a6acd18)
#14 2026-10-31T10:40:12.719Z decision g_bc3cbaec237464d10e22cbe6b3615f88 refused (arg_not_allowed): Grant does not allow argument status. Allowed: content, id, title. 1 use left.
Chain intact through entry 20.
Who issued it and why, what the agent tried, what was allowed, what the upstream said and how long it took. The last line covers the whole log, so you know the filtered view wasn’t built on a damaged chain. If you still have the agent’s original calls, from its own transcript for example, the digests tell you which call is which.
Know what the log never stores
Before you hand an export to anyone, it helps to know what’s not in it. We searched the full export from the case study session for the three grant secrets, a made-up token an unknown caller sent, the upstream key kept in the vault, and the values the agents sent: titles like “Release notes” and “Pricing 2027” and the status “published”. None of them appeared.
Request entries carry argument names (id, status, title) and a digest of the call. Grant entries carry the operator’s own reason text, so keep secrets out of --reason. The digests have no secret key, which means a short guessable value like a post id can be found by trying candidates. Treat an export like the audit log: fine for people who may know which records an agent touched, not for publishing.
The log also can’t see anything an agent did by another route. It’s evidence of what passed through Tokening, and that’s one more reason the real credential should live only there.