Skip to content
Tenbri pangolinTenbri

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>.

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 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); id and ts may be given. See Entry format.
  • Before appending, the writer writes a reconcile entry (status: unknown) for every intent open longer than 1 hour, except the intent this draft closes.
  • tenbri append does not check action schemas; tenbrid and tenbri schema check do.
  • A draft that does not parse prints tenbri: stdin: <code>: <detail>; one the writer refuses prints tenbri: 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}

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).

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-dc736478ac6b04bad25e3a3a6f8d71aa
latest checkpoint: size 4

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

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>.note
timestamped by <URL>: anchors/<n, 12 digits>.tsr

The second line appears with --tsa; if the checkpoint already has a token, it reads already timestamped: anchors/<n, 12 digits>.tsr instead.

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.

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 entries
timestamped by <tsa> at <time> (RFC 3161): the checkpoint existed then

One 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.

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 7

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>

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.

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).

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
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.