tenbri CLI
tenbri creates streams, appends entries, verifies, queries and follows them, seals checkpoints,
makes and checks receipts, manages data keys and erasure, and checks streams against action
schemas. It ships with the keychain and s3 Cargo features on by default.
tenbri [--keys DIR] <COMMAND>All output described below goes to stdout. Errors go to stderr as tenbri: <message>.
Global options
Section titled “Global options”| Flag | Value | Default | Meaning |
|---|---|---|---|
--keys |
DIR |
$TENBRI_KEYS, else $XDG_CONFIG_HOME/tenbri/keys, else ~/.config/tenbri/keys (%APPDATA%\tenbri\keys on Windows) |
Keys directory. Never inside a stream. |
--help |
Print help. | ||
--version |
Print the version. |
Commands that need keys (init, append, checkpoint, erase, keys) fail with exit code 2 and
no keys directory: pass --keys or set TENBRI_KEYS when no directory can be found.
A stream PATH is a local directory (<root>/<stream>) or an object-store URL
(s3://bucket/<root>/<stream>, r2://bucket/<root>/<stream>). See
Storage.
A --key value is a fingerprint sha256:<64 hex> or a public key ed25519:<64 hex>; a public key
is turned into its fingerprint. Anything else fails with
--key <value>: expected sha256:<64 hex> or ed25519:<64 hex> (exit 2).
Create a stream and its stream key; print the fingerprint to pin.
tenbri init <PATH> [--keychain]| Argument or flag | Meaning |
|---|---|
PATH |
A directory that does not exist or is empty, or an empty object-store prefix |
--keychain |
Keep the secret keys in the OS keychain (macOS Keychain, Windows Credential Manager) instead of key files. Only with the keychain feature. |
init writes the genesis entry (the stream key certifying itself) and a session key certification,
so a new stream holds 2 entries. It fails (exit 2) with <PATH> is not empty for a used directory,
<PATH> already holds a stream when a HEAD exists, and when the keys directory and the stream
directory contain each other.
Output:
created stream <stream id> at <PATH>stream key fingerprint (pin it): <sha256:...>keys: <key directory>verify with: tenbri verify <PATH> --key <sha256:...>append
Section titled “append”Append one entry, read as JSON on stdin; print it as stored.
tenbri append <PATH> < entry.json- The input is a draft: canonicalised, with duplicate keys and non-integers rejected, never
repaired. It must not contain the members the writer fills (
v,stream,seq,prev,signer,sig);idandtsmay be given. See Entry format. - Before appending, the writer writes a
reconcileentry (status: unknown) for every intent open longer than 1 hour, except the intent this draft closes. tenbri appenddoes not check action schemas;tenbridandtenbri schema checkdo.- A draft that does not parse prints
tenbri: stdin: <code>: <detail>; one the writer refuses printstenbri: entry rejected: <code>: <detail>. Both exit 2; nothing is written.
Output: the stored entry, canonical JSON on one line.
{"action":{"target":"t:1","type":"reply_ticket"},"id":"...","kind":"decision",...,"v":1}verify
Section titled “verify”Check canonical bytes, chain, signatures and keys against a pinned key.
tenbri verify <PATH> [--key FINGERPRINT] [--checkpoint FILE]| Flag | Meaning |
|---|---|
--key FINGERPRINT |
Fingerprint (sha256:...) or public key (ed25519:...) of the stream’s genesis key |
--checkpoint FILE |
A checkpoint (C2SP signed note) seen before. The stream must still hold exactly the entries it covers, so truncation and rewrites behind it are caught. |
When the stream has checkpoints of its own, verify also checks it against its latest one.
Output, in the order printed:
| Case | Lines | Exit |
|---|---|---|
| Stream broken | broken at seq <n>: <code>: <detail>, then <k> entries verified before it, stream <id> |
1 |
--checkpoint matches |
matches checkpoint <origin> at size <size> |
continues |
--checkpoint does not match |
broken at seq <n>: <code>: <detail>, then against checkpoint <origin> at size <size>; the stream has <k> entries |
1 |
| Stream has its own checkpoint | matches its latest checkpoint, at size <size> |
continues |
Pinned with --key |
verified <k> entries, stream <id>, pinned to <fingerprint> |
0 |
No --key |
consistent: <k> entries, stream <id>, then NOT pinned: this only shows the stream agrees with its own genesis key; pass --key <fingerprint> |
3 |
A malformed --checkpoint file exits 2. A checkpoint does not replace --key: without it the exit
code is still 3. Codes are listed in Errors.
Print the entries matching every filter given, in sequence order, one JSON per line, each with its verification status, seq, entry hash and what the answer is current to.
tenbri query <PATH> [--input REF@VERSION]... [--action TYPE] [--principal KIND:ID] [--target TARGET] [--subject HANDLE] [--rule REF@VERSION] [--since TIME] [--until TIME] [--trace TRACE_ID] [--status STATUS] [--after SEQ] [--limit N] [--verify] [--key FINGERPRINT]| Flag | Meaning |
|---|---|
--input REF@VERSION |
An input the entry used, as ref@version (policy:refund@14), or ref for any version. Repeat to require several. |
--action TYPE |
The action type (reply_ticket) |
--principal KIND:ID |
The principal (agent:support-assistant) |
--target TARGET |
The action target, exactly |
--subject HANDLE |
The subject handle |
--rule REF@VERSION |
The rule, as ref@version, or ref for any version |
--since TIME |
Entries at or after this time: 2026-10-20T09:00:00.000Z, 2026-10-20T09:00:00Z, 2026-10-20 (midnight UTC), or a duration ago (90s, 15m, 1h, 7d). Compared with ts, the writer’s claimed time. |
--until TIME |
Entries before this time, in the same forms |
--trace TRACE_ID |
The OpenTelemetry trace id |
--status STATUS |
An outcome’s status: ok, error, refused or unknown |
--after SEQ |
Only entries after this sequence number, the cursor of the previous page. Default 0. |
--limit N |
At most this many entries. When more match, tenbri: more entries match; next page: --after <seq> goes to stderr. |
--verify |
Check the index’s answers against the stream, read and verified in full |
--key FINGERPRINT |
Pin the stream’s genesis key |
Every filter given must match. Output, one line per matching entry:
{"verification":"indexed","entry":{...},"seq":4,"hash":"sha256:bcf0853b…","as_of":{"stream":"tnb1:dc736478ac6b04bad25e3a3a6f8d71aa","seq":4,"hash":"sha256:bcf0853b…","checkpoint":4}}seq and hash are the entry’s sequence number and entry hash. as_of is what the answer is
current to: the stream, the last sequence number read, its entry hash and, from an index, the size
of the latest checkpoint at or below it.
Where the answer comes from decides verification:
| Case | How query reads |
verification |
|---|---|---|
| No index | Reads and verifies the whole stream | verified with --key, unpinned without |
| An index (see index rebuild) | Brings the index up to HEAD, then answers from it | indexed |
An index and --verify |
Also reads and verifies the whole stream and checks each answer’s entry hash against it | verified with --key, unpinned without |
If the stream does not verify, query prints tenbri: the stream does not verify: broken at seq <n>: ... and exits 1. With --verify, an index that disagrees with the stream exits 1 with
the index disagrees with the stream at seq <n>: run `tenbri index rebuild` . A time that does
not parse exits 2 with --since <value>: expected a time (2026-10-20T09:00:00Z, 2026-10-20) or a duration (1h).
index rebuild
Section titled “index rebuild”Delete the stream’s index and build it again from every batch.
tenbri index rebuild <PATH> [--backend sqlite|parquet]| Flag | Default | Meaning |
|---|---|---|
--backend |
sqlite |
sqlite (one file, index.sqlite) or parquet (a parquet/ directory any Arrow tool reads) |
The index is a derived view that query uses; deleting it loses nothing. It lives in
$TENBRI_INDEX_DIR, else the user’s cache directory (~/Library/Caches/tenbri/index on macOS,
$XDG_CACHE_HOME/tenbri/index or ~/.cache/tenbri/index elsewhere, %LOCALAPPDATA%\tenbri\index
on Windows), one directory per stream named after its id with : as -. Once it exists, every
query brings it up to HEAD first. Entries of a subject whose keys were erased are left out, and
tenbri erase deletes that subject’s rows from an existing index.
Output:
indexed 4 entries of tnb1:dc736478ac6b04bad25e3a3a6f8d71aa (sqlite) at <index dir>/tnb1-dc736478ac6b04bad25e3a3a6f8d71aalatest checkpoint: size 4The second line appears when the stream has a checkpoint.
Print the last entries, then follow new ones as they are written.
tenbri tail <PATH> [-n LINES] [--no-follow] [--key FINGERPRINT]| Flag | Default | Meaning |
|---|---|---|
-n LINES |
10 |
How many existing entries to print first |
--no-follow |
off | Print and exit instead of following |
--key FINGERPRINT |
Pin the stream’s genesis key |
Output: each entry as canonical JSON on one line. While following, tail polls the stream every
500 ms. A stream that stops verifying exits 1.
checkpoint
Section titled “checkpoint”Seal and sign a checkpoint of the whole stream now; with --tsa, timestamp it (RFC 3161).
tenbri checkpoint <PATH> [--tsa URL]| Flag | Meaning |
|---|---|
--tsa URL |
URL of an RFC 3161 timestamp authority. Its token is stored next to the checkpoint (anchors/<size>.tsr) and goes into receipts. |
Output:
checkpoint at size <n>: checkpoints/<n, 12 digits>.notetimestamped by <URL>: anchors/<n, 12 digits>.tsrThe second line appears with --tsa; if the checkpoint already has a token, it reads
already timestamped: anchors/<n, 12 digits>.tsr instead.
receipt create
Section titled “receipt create”Write a receipt for entries of a stream: the entries, the stream’s key history, a signed checkpoint covering them with their inclusion proofs, and its RFC 3161 token if it has one.
tenbri receipt create <PATH> --entry SEQ|ID [--entry ...] [--out FILE] [--key FINGERPRINT]| Flag | Default | Meaning |
|---|---|---|
--entry <SEQ or ID> |
required | An entry’s sequence number or id; repeat for a receipt proving several |
--out FILE |
stdout | Where to write the receipt |
--key FINGERPRINT |
Check the stream against this fingerprint while reading it |
Exits 1 with the stream does not verify: ... when the stream is broken.
receipt verify
Section titled “receipt verify”Check a receipt offline against a pinned stream key: no network, no access to the stream.
tenbri receipt verify <FILE> --key FINGERPRINT [--tsa-roots PEM]| Flag | Meaning |
|---|---|
--key FINGERPRINT |
Required. Fingerprint or public key of the stream’s genesis key. |
--tsa-roots PEM |
Root certificates of the timestamp authorities you trust. Without it, RFC 3161 tokens in the receipt are not checked. |
Output when it verifies (exit 0):
proves seq <n> (<kind> [<phase>]), id <id>, ts <ts> (claimed by the writer)in stream <origin>, checkpoint at size <size>, pinned to <fingerprint>; key history: <k> key entriestimestamped by <tsa> at <time> (RFC 3161): the checkpoint existed thenOne proves line per entry; one timestamped line per token with --tsa-roots. Without
--tsa-roots, a receipt with tokens prints
<k> RFC 3161 token(s) NOT checked: pass --tsa-roots <pem> to trust a timestamp authority. A
receipt that fails prints receipt does not verify: <reason> and exits 1.
schema check
Section titled “schema check”Validate tenbri.toml, then every entry of the given streams against it.
tenbri schema check [PATHS]... [--schema FILE]| Argument or flag | Default | Meaning |
|---|---|---|
PATHS |
none | Streams to check: directories or s3:// / r2:// URLs |
--schema FILE |
tenbri.toml |
The schema file |
Output and exit codes are in Action schemas.
Delete a subject’s data keys, so its bodies and input digests can no longer be read or confirmed, and record the erasure in the stream. The chain, checkpoints and receipts still verify.
tenbri erase <PATH> (--subject HANDLE | --tenant) --reason REASON [--by ID] [--yes]| Flag | Meaning |
|---|---|
--subject HANDLE |
The subject whose data keys to delete |
--tenant |
The tenant’s keys, used by entries without a subject |
--reason REASON |
Required. A reference such as the request id (dsr:2026-1042), recorded in the erasure note. |
--by ID |
Who asked for it, recorded as the note’s principal (a person id) |
--yes |
Do not ask for confirmation |
Exactly one of --subject and --tenant. Without --yes, erase asks you to type the handle (or
tenant); anything else exits 2 with not confirmed; nothing was erased.
The stream must verify and this machine must hold a key to sign with before anything is deleted.
With --subject, the subject’s rows are also deleted from the stream’s query index, if it has
one.
Output: erased <subject HANDLE | tenant>: <n> data key epochs deleted; erasure recorded at seq <seq>.
erased subject subj_7f3c91d2: 2 data key epochs deleted; erasure recorded at seq 7keys list
Section titled “keys list”List the signing keys and the data keys (subjects and key epochs); never a secret.
tenbri keys list <PATH>Output:
signing keys of <stream id> (<key directory>): <key id> <stream|session> <ed25519:...>[ until <not_after>]data keys (<data key directory>): <subject HANDLE | tenant>: epochs <first>..<last>, newest made <created>keys export
Section titled “keys export”Print a subject’s data keys as JSON, for a backup kept apart from the log.
tenbri keys export <PATH> (--subject HANDLE | --tenant) [--digest]| Flag | Meaning |
|---|---|
--digest |
Only the digest keys: enough to confirm input digests, never to open a body |
Output: {"epochs": [{"created", "epoch", "key"}...], "stream", "subject"}, pretty-printed; with
--digest, each epoch has digest_key instead of key. With no key held (erased, or never made
here), it exits 2 with no data key for subject "<HANDLE>": erased, or never held here.
keys rotate
Section titled “keys rotate”Start a new data key epoch for a subject and record it in the stream. Older epochs keep opening older data until erased.
tenbri keys rotate <PATH> (--subject HANDLE | --tenant) [--by ID]| Flag | Meaning |
|---|---|
--by ID |
Who rotated, recorded as the note’s principal (a person id) |
Output: <subject HANDLE | tenant>: new data key epoch <n>, recorded at seq <seq>. These are data
keys; the CLI does not rotate signing keys (Keys).
Exit codes
Section titled “Exit codes”Stable from 0.1.0.
| Code | Meaning |
|---|---|
0 |
Success |
1 |
The stream does not verify, or does not match the --checkpoint given (truncated behind it, rewritten, or not signed by the stream); the first broken sequence number is printed. Also: schema check found an entry that breaks its action’s schema; receipt verify found a receipt that does not verify; query, tail or receipt create read a stream that does not verify; query --verify found the index disagreeing with the stream. |
2 |
Any other error, a malformed checkpoint file among them |
3 |
verify without --key: the stream is consistent with its own genesis key, which proves nothing about who wrote it |
Environment variables
Section titled “Environment variables”| Variable | Used for |
|---|---|
TENBRI_KEYS |
Keys directory when --keys is not given |
XDG_CONFIG_HOME, HOME |
Default keys directory ($XDG_CONFIG_HOME/tenbri/keys, else ~/.config/tenbri/keys) |
APPDATA |
Default keys directory on Windows |
TENBRI_INDEX_DIR |
Where query indexes live |
XDG_CACHE_HOME, LOCALAPPDATA |
Default index directory ($XDG_CACHE_HOME/tenbri/index, else ~/.cache/tenbri/index; ~/Library/Caches/tenbri/index on macOS; %LOCALAPPDATA%\tenbri\index on Windows) |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION and the other AWS_* |
Credentials for s3:// and r2:// |
AWS_ENDPOINT_URL, AWS_ALLOW_HTTP |
S3-compatible endpoint (MinIO); AWS_ALLOW_HTTP=true for plain HTTP |
CLOUDFLARE_ACCOUNT_ID |
R2 endpoint, when AWS_ENDPOINT_URL is not set |
Details per backend are in Storage.
