Quickstart
In five minutes you create a stream in a local directory, record an intent and its outcome, verify
the stream against its pinned key, and query it. Pick the CLI, Python or TypeScript: the three
write the same stream format, so tenbri verify checks a stream written from code and the other
way round. Install what you pick first (Install).
The outputs below come from real runs on macOS. Your ids, keys, hashes, signatures and timestamps
will differ. Long values are shortened with ….
Pick a keys directory
Section titled “Pick a keys directory”Signing keys live outside the stream. For this walkthrough, use a keys directory next to it:
mkdir tenbri-demo && cd tenbri-demoexport TENBRI_KEYS=$PWD/keysWithout TENBRI_KEYS, every writer uses ~/.config/tenbri/keys.
Record, verify and query
Section titled “Record, verify and query”-
Create a stream.
Terminal window tenbri init audit/supportcreated stream tnb1:13410a435d5a70cc7f96ae05209410bb at audit/supportstream key fingerprint (pin it): sha256:13410a435d5a70cc7f96ae05209410bb37cd301124e0719229b3184530789b61keys: /…/tenbri-demo/keys/tnb1-13410a435d5a70cc7f96ae05209410bbverify with: tenbri verify audit/support --key sha256:13410a435d5a70cc7f96ae05209410bb37cd301124e0719229b3184530789b61The directory must be new or empty. Copy the fingerprint somewhere the operator of the stream cannot change it: it is your trust root.
-
Record an intent.
tenbri appendreads one entry as JSON on stdin. An intent is written before the action runs:Terminal window tenbri append audit/support <<'EOF'{"kind":"decision","phase":"intent","source":"sdk","principal":{"kind":"agent","id":"support-assistant"},"action":{"type":"refund","target":"order:8812"},"inputs":[{"ref":"policy:refund","version":"14","digest":"hmac-sha256:9f2c41aa9f2c41aa9f2c41aa9f2c41aa9f2c41aa9f2c41aa9f2c41aa9f2c41aa"}],"rule":{"ref":"refund.max_amount","version":"14","result":"allow"}}EOFIt prints the entry as stored, in canonical form (one line, keys sorted):
{"action":{"target":"order:8812","type":"refund"},"id":"01M5CWCEYW1GZWQE795VT6BGKE","inputs":[{"digest":"hmac-sha256:9f2c41aa…","ref":"policy:refund","version":"14"}],"kind":"decision","phase":"intent","prev":"sha256:b44fbc6c…","principal":{"id":"support-assistant","kind":"agent"},"rule":{"ref":"refund.max_amount","result":"allow","version":"14"},"seq":3,"sig":"ed25519:6258835f…","signer":"sess:2026-10-20:c4de0a227bf3","source":"sdk","stream":"tnb1:13410a435d5a70cc7f96ae05209410bb","ts":"2026-10-20T14:45:26.108Z","v":1}The writer fills in
v,id,stream,seq,ts,prev,signerandsig. You may giveidandtsyourself; giving the others is an error:tenbri: stdin: invalid_field: `seq` is filled in by the writerNote the entry’s
id. The outcome names it. -
Record its outcome.
Terminal window tenbri append audit/support <<'EOF'{"kind":"decision","phase":"outcome","source":"sdk","intent":"01M5CWCEYW1GZWQE795VT6BGKE","status":"ok","principal":{"kind":"agent","id":"support-assistant"},"action":{"type":"refund","target":"order:8812"}}EOF{"action":{"target":"order:8812","type":"refund"},"id":"01M5CWCF057ED20JDR0QJQ2ZR6","intent":"01M5CWCEYW1GZWQE795VT6BGKE","kind":"decision","phase":"outcome","prev":"sha256:465624fb…","principal":{"id":"support-assistant","kind":"agent"},"seq":4,"sig":"ed25519:d3240344…","signer":"sess:2026-10-20:c4de0a227bf3","source":"sdk","status":"ok","stream":"tnb1:13410a435d5a70cc7f96ae05209410bb","ts":"2026-10-20T14:45:26.149Z","v":1}An intent takes exactly one outcome. A second one for the same intent fails, writes nothing and exits 2:
tenbri: entry rejected: bad_intent: no open intent 01M5CWCEYW1GZWQE795VT6BGKE -
Verify against the pinned key.
Terminal window tenbri verify audit/support --key sha256:13410a435d5a70cc7f96ae05209410bb37cd301124e0719229b3184530789b61verified 4 entries, stream tnb1:13410a435d5a70cc7f96ae05209410bb, pinned to sha256:13410a435d5a70cc7f96ae05209410bb37cd301124e0719229b3184530789b61The exit code is 0. Without
--key,verifycan only say the stream agrees with its own genesis key, and exits 3:consistent: 4 entries, stream tnb1:13410a435d5a70cc7f96ae05209410bbNOT pinned: this only shows the stream agrees with its own genesis key; pass --key <fingerprint> -
Query by input.
Find every entry that used version 14 of the refund policy:
Terminal window tenbri query audit/support --input policy:refund@14 --key sha256:13410a43…{"verification":"verified","entry":{"action":{"target":"order:8812","type":"refund"},"id":"01M5CWCEYW1GZWQE795VT6BGKE",…,"phase":"intent",…,"seq":3,…},"seq":3,"hash":"sha256:465624fb…","as_of":{"stream":"tnb1:13410a435d5a70cc7f96ae05209410bb","seq":4,"hash":"sha256:467d3d96…"}}Each line is one match: the entry, its
verification(verifiedwith--key,unpinnedwithout), its hash, andas_of, the head of the stream the answer was read from.--action refundselects by action type and matches both entries. Repeat--inputto require several. -
Tail the stream.
Terminal window tenbri tail audit/support -n 2 --no-follow{"action":{"target":"order:8812","type":"refund"},"id":"01M5CWCEYW1GZWQE795VT6BGKE",…,"phase":"intent",…,"seq":3,…}{"action":{"target":"order:8812","type":"refund"},"id":"01M5CWCF057ED20JDR0QJQ2ZR6","intent":"01M5CWCEYW1GZWQE795VT6BGKE",…,"phase":"outcome",…,"seq":4,…}Without
--no-follow,tailkeeps running and prints new entries as they are written.
Needs pip install --pre tenbri (Python 3.10 or later).
from tenbri import Tenbri
# Creates the stream and its keys (in $TENBRI_KEYS) the first time.log = Tenbri.open("audit/support", create=True, digest_key=b"a 32-byte secret, kept apart....")pin = log.fingerprintprint("created", log.id)print("pin this:", pin)
# The intent: durable before the refund runs.intent = log.intent( principal="agent:support-assistant", action={"type": "refund", "target": "order:8812"}, inputs=[{ "ref": "policy:refund", "version": "14", "digest": log.digest("refunds under 50 EUR are automatic"), }], rule={"ref": "refund.max_amount", "version": "14", "result": "allow"},)print("intent ", intent["seq"], intent["id"])
# ... issue the refund here ...
outcome = log.outcome(intent, "ok")print("outcome", outcome["seq"], "closes", outcome["intent"])log.close()
# Later, or on another machine: open the stream pinned to the fingerprint you kept.with Tenbri.open("audit/support", key=pin) as log: report = log.verify() print("verified:", report.ok, report.entries, "entries") for m in log.query(inputs=["policy:refund@14"]): print(m.verification, m.entry["seq"], m.entry["phase"], m.entry["action"])python quickstart.pycreated tnb1:dc978283284e60b2751c968433a059d5pin this: sha256:dc978283284e60b2751c968433a059d5f643ac06fdc7da25553a613fb313ae90intent 3 01M5CX09691W9VTDNT2HPDEV6Foutcome 4 closes 01M5CX09691W9VTDNT2HPDEV6Fverified: True 4 entriesverified 3 intent {'target': 'order:8812', 'type': 'refund'}create=Truemakes the stream and its keys when there is none, and opens it otherwise: a second run appends seq 5 and 6.intent()fills inkind,phaseandsource.outcome()copies the intent’s principal and action. Both return the entry as stored, once it is durable.log.digest(content)is the keyed digest (hmac-sha256:…) of what the agent read, with thedigest_keygiven at open. Keep that key in your secret store.verify()is onlyokagainst a pinned key. Opened withoutkey=, query results sayunpinned.
Needs npm install tenbri@next (Node 20 or later). Node 22.18 and later run TypeScript files
directly; on Node 20, run npx tsx quickstart.ts.
npm pkg set type=moduleimport { Tenbri } from 'tenbri'
// Creates the stream and its keys (in $TENBRI_KEYS) the first time.const log = await Tenbri.open('audit/support', { create: true, digestKey: 'a 32-byte secret, kept apart....' })const pin = log.fingerprint!console.log('created', log.id)console.log('pin this:', pin)
// The intent: durable before the refund runs.const intent = await log.intent({ principal: 'agent:support-assistant', action: { type: 'refund', target: 'order:8812' }, inputs: [{ ref: 'policy:refund', version: '14', digest: log.digest('refunds under 50 EUR are automatic') }], rule: { ref: 'refund.max_amount', version: '14', result: 'allow' },})console.log('intent ', intent.seq, intent.id)
// ... issue the refund here ...
const outcome = await log.outcome(intent, 'ok')console.log('outcome', outcome.seq, 'closes', outcome.intent)await log.close()
// Later, or on another machine: open the stream pinned to the fingerprint you kept.const pinned = await Tenbri.open('audit/support', { key: pin })const report = await pinned.verify()console.log('verified:', report.ok, report.entries, 'entries')for (const m of await pinned.query({ inputs: ['policy:refund@14'] })) { console.log(m.verification, m.entry.seq, m.entry.phase, m.entry.action)}await pinned.close()node quickstart.tscreated tnb1:02466ddba1f14e3dd6e610c99a1da7d0pin this: sha256:02466ddba1f14e3dd6e610c99a1da7d08698dee13221c6e76b380f0af2c3105aintent 3 01M5CWH0J5FWAD534HXDQDCPR7outcome 4 closes 01M5CWH0J5FWAD534HXDQDCPR7verified: true 4 entriesverified 3 intent { target: 'order:8812', type: 'refund' }create: truemakes the stream and its keys when there is none, and opens it otherwise.intent()fills inkind,phaseandsource.outcome()copies the intent’s principal and action. Both resolve to the entry as stored, once it is durable.log.digest(content)is the keyed digest (hmac-sha256:…) of what the agent read, with thedigestKeygiven at open. Keep that key in your secret store.verify()is onlyokagainst a pinned key. Opened withoutkey, query results sayunpinned.
Seq 1 and 2 are the key entries that creating the stream wrote: the genesis entry (the stream key
certifying itself) and the certification of the session key that signs everything after. The
stream id is tnb1: and the first 32 hex digits of the fingerprint.
Check it from the CLI
Section titled “Check it from the CLI”Whichever way you wrote the stream, the CLI verifies it:
tenbri verify audit/support --key sha256:dc978283284e60b2751c968433a059d5f643ac06fdc7da25553a613fb313ae90verified 4 entries, stream tnb1:dc978283284e60b2751c968433a059d5, pinned to sha256:dc978283284e60b2751c968433a059d5f643ac06fdc7da25553a613fb313ae90See tampering fail
Section titled “See tampering fail”Change one byte of entry 3 in a copy of the stream, then verify the copy:
cp -r audit/support tamperedsed -i.bak 's/order:8812/order:8813/' tampered/batches/000000000003.jsonlrm tampered/batches/*.baktenbri verify tampered --key sha256:13410a43…broken at seq 3: bad_signature: the signature does not verify2 entries verified before it, stream tnb1:13410a435d5a70cc7f96ae05209410bbThe exit code is 1. query on a stream that does not verify also exits 1. From code, verify()
returns a report with ok false and the break. In Python, print(r.ok, r.broken) after
r = Tenbri.open("tampered", key=pin).verify() shows:
False Broken(seq=3, code='bad_signature', message='the signature does not verify')What is on disk
Section titled “What is on disk”tenbri-demo/ audit/support/ .lock held while HEAD is swapped; not part of the stream HEAD batches/000000000001.jsonl batches/000000000002.jsonl batches/000000000003.jsonl batches/000000000004.jsonl keys/tnb1-13410a435d5a70cc7f96ae05209410bb/ 13410a435d5a.json the stream key's public description 13410a435d5a.secret its secret (0600) c4de0a227bf3.json the session key's public description c4de0a227bf3.secretEach CLI call writes its own batch file. HEAD names the latest entry and is signed by the
session key.
- Write from code: runs, reads and decisions as context managers, callbacks and decorators.
- Streams and entries: every field you can set.
- Integrity: what
verifychecks. - CLI reference and error codes.
