CLI reference.
GAEverything TillNotary does is scriptable through tilldev notary. Every command that reads the log checks what it gets back against your pinned trust file before it prints anything: signatures, the cosignature threshold, and the Merkle proof. There is no mode that just shows what the server said. The output on this page is real.
Install, flags & environment
$ npm i -g @tillstack/cli
$ export TILLDEV_NOTARY_TOKEN=tnot_… # from the dashboard; omit for public reads
$ export TILLDEV_NOTARY_TRUST_FILE=notary-trust.json| Flag | Environment variable | What it does |
|---|---|---|
--trust <file> | TILLDEV_NOTARY_TRUST_FILE | The pinned trust file. Every command that verifies needs it; trust-init writes it. |
--notary-url <url> | TILLDEV_NOTARY_URL | Service URL. Defaults to the trust file’s base_url, then https://notary.tilldev.dev. Must be https. |
--token <tnot_…> | TILLDEV_NOTARY_TOKEN | Service token. Prefer the environment variable: flags end up in shell history and process lists. |
--json | — | Machine-readable output on every command. Failures still exit non-zero. |
Public logs can be read and verified with no token. Submitting needs a submit token, and managing logs and witnesses needs admin; see Service tokens.
trust-init: pin the keys, then check them
trust-init <log> [--out <file>] [--prune] fetches the log’s key, its witnesses and their threshold, writes them to the trust file, and prints each key’s fingerprint:
$ tilldev notary trust-init acme-releases
OK Pinned acme-releases into notary-trust.json
origin notary.tilldev.dev/acme-releases
log key fp 1388 3e7f fc10 01f9 32c0 e322 13f6 aba7
witness acme-witness/primary d3ee 0e5f a5f0 f399 a298 2f58 3ad7 134b
witness tillnotary-witness/w1 29ac d643 062e 5a23 ed8c 7c94 ea41 bc43
required 2
! TRUST ON FIRST USE: new keys came from the log itself. Verify the fingerprints out of band (dashboard, ops channel) before relying on this pin — a pinned lie stays a lie.Re-running trust-init only ever adds: new witnesses are pinned, a higher threshold is adopted, and nothing you pinned is dropped without you saying so. A witness that left the log stays pinned so the evidence it cosigned keeps verifying, and a lower threshold is reported but not adopted. Pass --prune to accept removals and a lower threshold; do that at once if a witness key was compromised.
# the log dropped a witness and lowered its threshold; your pin does not follow on its own
$ tilldev notary trust-init acme-releases
OK Re-pinned acme-releases into notary-trust.json
origin notary.tilldev.dev/acme-releases
log key fp 1388 3e7f fc10 01f9 32c0 e322 13f6 aba7
witness tillnotary-witness/w1 29ac d643 062e 5a23 ed8c 7c94 ea41 bc43
witness acme-witness/primary d3ee 0e5f a5f0 f399 a298 2f58 3ad7 134b (kept: no longer attached)
required 2
* Kept acme-witness/primary so evidence it cosigned still verifies. --prune removes it (do that if its key was compromised).
! The log now requires 1 cosignature(s); your pin keeps requiring 2. --prune accepts the lower number.
$ tilldev notary trust-init acme-releases --prune
OK Re-pinned acme-releases into notary-trust.json
origin notary.tilldev.dev/acme-releases
log key fp 1388 3e7f fc10 01f9 32c0 e322 13f6 aba7
witness tillnotary-witness/w1 29ac d643 062e 5a23 ed8c 7c94 ea41 bc43
witness acme-witness/primary removed (--prune)
required 1The trust-file format
Plain JSON, so a change to a witness set shows up in code review (keys shortened here):
{
"base_url": "https://notary.tilldev.dev",
"logs": {
"acme-releases": {
"origin": "notary.tilldev.dev/acme-releases",
"log_key_name": "tillnotary-log/acme-releases",
"public_key": "HXNpZy1lZDI1NTE5LW1sZHNhNjUtc2hhMjU2LnYxZsEDd9Ww8HxM…",
"witnesses": [
{ "key_name": "acme-witness/primary", "public_key": "HXNpZy1lZDI1NTE5LW1sZHNhNjUtc2hhMjU2LnYx+t3t/oZP5Cwt…" },
{ "key_name": "tillnotary-witness/w1", "public_key": "HXNpZy1lZDI1NTE5LW1sZHNhNjUtc2hhMjU2LnYxKwBN8HIl+wey…" }
],
"required_cosignatures": 2
}
}
}required_cosignatures is your floor. A checkpoint with fewer valid cosignatures from the witnesses you pinned is refused, whatever the server says. The Node SDK reads the same file.
submit: add an entry
submit <log> --hash <hex> | --file <path> [--attest] [--out <bundle.json>] adds one entry and checks its inclusion proof before reporting success. With --file the CLI hashes the file with the log’s hash function and sends only the digest. With --attest the bytes themselves are stored and served publicly (4096 bytes at most). --out writes an evidence bundle.
# hash a file locally and submit only the digest; the file never leaves your machine
$ tilldev notary submit acme-releases --file app-v2.4.1.tar.gz --out receipt.json
OK Logged at index 5 of notary.tilldev.dev/acme-releases — receipt VERIFIED locally
leaf c53d57898e94df8d3fd05e41456bb17357992da9c36bab87edda8fe75f9acafb
origin notary.tilldev.dev/acme-releases
tree size 6
root af3c6bd3a607272651fafbc9e369219a79dd3e0f84d9719b4ab77a1f22c2219e
timestamp 2026-09-27T05:38:01.200Z
cosignatures —
status incomplete — awaiting witness
OK Evidence bundle written to receipt.json
! Receipt is not yet witness-complete — re-run `tilldev notary proof` later to export a complete bundle.
# a digest you computed yourself (64 hex chars; 96 on the gov suite)
$ tilldev notary submit acme-releases --hash a70830e7dd9fd50a60628009e57090311ba4efaf9708594f52501ebc3b6d7867
# publish the bytes themselves, up to 4096 (they become publicly readable)
$ tilldev notary submit acme-releases --file sbom-summary.json --attestA new entry is in the log at once, but witnesses cosign on their own schedule, so a fresh receipt usually reads incomplete. Export a complete bundle with proof after their next pass. Submitting a value that is already in the log returns its original receipt rather than a second entry, which makes submit safe to retry:
$ tilldev notary submit acme-releases --file app-v2.4.1.tar.gz
OK Already logged at index 5 of notary.tilldev.dev/acme-releases — original receipt, VERIFIED locally
leaf c53d57898e94df8d3fd05e41456bb17357992da9c36bab87edda8fe75f9acafb
origin notary.tilldev.dev/acme-releases
tree size 6
root af3c6bd3a607272651fafbc9e369219a79dd3e0f84d9719b4ab77a1f22c2219e
timestamp 2026-09-27T05:38:01.200Z
cosignatures tillnotary-witness/w1, acme-witness/primary
status COMPLETE (witness-cosigned)proof & verify: inclusion, online and offline
proof <log> <index> [--out <bundle.json>] fetches and checks a leaf’s inclusion proof, prints the path, and optionally writes a portable evidence bundle:
$ tilldev notary proof acme-releases 5 --out bundle.json
OK VERIFIED — leaf 5 of notary.tilldev.dev/acme-releases
leaf c53d57898e94df8d3fd05e41456bb17357992da9c36bab87edda8fe75f9acafb
content class hash-only
origin notary.tilldev.dev/acme-releases
tree size 8
root 96b66e36e50a3a61fc37f08bf11e12d2b6ea922068183a8d24e1ae613b98402d
timestamp 2026-09-27T05:38:02.349Z
cosignatures tillnotary-witness/w1, acme-witness/primary
status COMPLETE (witness-cosigned)
inclusion path (3 nodes, leaf→root):
b153ffc56f36cbfd7bb208f43603fe704c27a0b9424c0424c94fd99f2cc539a3
ed3fcf4624cb77b4775ec5717351c2907a92635984536725bb9056e89b0e2394
a1d55691a4764f1c797f8849de205858215b2113e6f0dca650bc5c85f428a743
OK Evidence bundle written to bundle.jsonverify <log> <index> runs the same checks as a pass/fail gate, and verify <bundle.json> (or --offline <file>) checks a bundle with no network at all. Any failure exits 1 with the reason, so either form can gate a CI job:
# online: fetch the leaf, a fresh proof and the checkpoint; verify all of it here
$ tilldev notary verify acme-releases 5
OK VERIFIED — leaf 5 of notary.tilldev.dev/acme-releases
leaf c53d57898e94df8d3fd05e41456bb17357992da9c36bab87edda8fe75f9acafb
content class hash-only
origin notary.tilldev.dev/acme-releases
tree size 8
root 96b66e36e50a3a61fc37f08bf11e12d2b6ea922068183a8d24e1ae613b98402d
timestamp 2026-09-27T05:38:02.349Z
cosignatures tillnotary-witness/w1, acme-witness/primary
status COMPLETE (witness-cosigned)
# offline: a bundle path instead of <log> <index>; no network at all
$ tilldev notary verify bundle.json
OK VERIFIED (offline) — leaf 5 of notary.tilldev.dev/acme-releases
leaf c53d57898e94df8d3fd05e41456bb17357992da9c36bab87edda8fe75f9acafb
origin notary.tilldev.dev/acme-releases
tree size 8
root 96b66e36e50a3a61fc37f08bf11e12d2b6ea922068183a8d24e1ae613b98402d
timestamp 2026-09-27T05:38:02.349Z
cosignatures tillnotary-witness/w1, acme-witness/primary
status COMPLETE (witness-cosigned)
# one changed character in the leaf
$ tilldev notary verify tampered.json
x VERIFICATION FAILED: inclusion proof INVALID for leaf 5 of notary.tilldev.dev/acme-releases at size 8
$ echo $?
1verify only passes a leaf once a checkpoint that includes it carries your pinned number of cosignatures. Right after a submit that has usually not happened yet. --latest accepts the newer, not yet cosigned head and says so:
$ tilldev notary verify acme-releases 8
x leaf 8 is only in checkpoints with 1 valid cosignature(s); trust requires 2. Witnesses cosign on their next pass
$ tilldev notary verify acme-releases 8 --latest
OK VERIFIED — leaf 8 of notary.tilldev.dev/acme-releases
…
cosignatures tillnotary-witness/w1
status incomplete — awaiting witness
! Not yet witness-complete: no witness-cosigned head includes this leaf yet.When you have the content but not its index, lookup <log> --file <f> (or --hash <hex>) finds the entry holding it and verifies it the same way. It is as strict as verify, takes --latest and --out <bundle.json>, and exits 1 when the log holds no such entry, so a script can tell “never logged” from “logged”:
# hashes the file with the log's suite, then fetches and verifies the entry holding it
$ tilldev notary lookup acme-releases --file app-v2.4.1.tar.gz
OK FOUND and VERIFIED — leaf 5 of notary.tilldev.dev/acme-releases
leaf c53d57898e94df8d3fd05e41456bb17357992da9c36bab87edda8fe75f9acafb
content class hash-only
origin notary.tilldev.dev/acme-releases
tree size 8
root 96b66e36e50a3a61fc37f08bf11e12d2b6ea922068183a8d24e1ae613b98402d
timestamp 2026-09-27T05:38:02.349Z
cosignatures tillnotary-witness/w1, acme-witness/primary
status COMPLETE (witness-cosigned)
$ tilldev notary lookup acme-releases --hash eb57be5f9fb398dfcdcf0cf070074282614b58ddff74e9fc1cd7a1885e1130a7
x Not logged: no entry in notary.tilldev.dev/acme-releases holds eb57be5f9fb398dfcdcf0cf070074282614b58ddff74e9fc1cd7a1885e1130a7
$ echo $?
1checkpoint, consistency, witness, note, refusals
checkpoint <log> [--latest] fetches and checks the newest checkpoint your witnesses have cosigned. --latest shows the newest head even before they have:
$ tilldev notary checkpoint acme-releases
OK Checkpoint VERIFIED for notary.tilldev.dev/acme-releases
origin notary.tilldev.dev/acme-releases
tree size 8
root 96b66e36e50a3a61fc37f08bf11e12d2b6ea922068183a8d24e1ae613b98402d
timestamp 2026-09-27T05:38:02.349Z
cosignatures tillnotary-witness/w1, acme-witness/primary
status COMPLETE (witness-cosigned)
# --latest: the newest head, even before the witnesses cosign it
$ tilldev notary checkpoint acme-releases --latest
OK Checkpoint VERIFIED for notary.tilldev.dev/acme-releases
origin notary.tilldev.dev/acme-releases
tree size 9
root 1c47b10066caca587b9cb48b04ad380cae1b983bb8eed67c74e24e84532b3bc2
timestamp 2026-09-27T05:44:15.554Z
cosignatures tillnotary-witness/w1
status incomplete — awaiting witnessconsistency <log> --from <size> [--from-root <hex>] proves the log today extends a head you recorded earlier. This is the command to run on a schedule. Without --from-root the old root comes from the log’s own signed history, which proves the log agrees with itself; pass the root from your stored receipt to prove it agrees with you.
# pass the root you recorded, so the check is bound to what you actually saw
$ tilldev notary consistency acme-releases --from 4 --from-root a1d55691a4764f1c797f8849de205858215b2113e6f0dca650bc5c85f428a743
OK APPEND-ONLY HOLDS: size 4 → 8 of notary.tilldev.dev/acme-releases
from root a1d55691a4764f1c797f8849de205858215b2113e6f0dca650bc5c85f428a743
origin notary.tilldev.dev/acme-releases
tree size 8
root 96b66e36e50a3a61fc37f08bf11e12d2b6ea922068183a8d24e1ae613b98402d
timestamp 2026-09-27T05:38:02.349Z
cosignatures tillnotary-witness/w1, acme-witness/primary
status COMPLETE (witness-cosigned)
# a root the log never had at that size
$ tilldev notary consistency acme-releases --from 6 --from-root abab…abab
x CONSISTENCY FAILED for notary.tilldev.dev/acme-releases: the tree at size 8 does NOT extend your checkpoint at size 6 — treat as compromisedwitness <log> lists the log’s witnesses, whether each has cosigned the newest head, and their key fingerprints. It warns when the threshold is higher than the number of active witnesses, because then no checkpoint can complete.
$ tilldev notary witness acme-releases
OK Witness policy for notary.tilldev.dev/acme-releases — threshold 2
WITNESS ACTIVE COSIGNED @8 KEY FP
--------------------- ------ ----------- -------------------
acme-witness/primary yes yes d3ee 0e5f a5f0 f399
tillnotary-witness/w1 yes yes 29ac d643 062e 5a23
origin notary.tilldev.dev/acme-releases
tree size 8
root 96b66e36e50a3a61fc37f08bf11e12d2b6ea922068183a8d24e1ae613b98402d
timestamp 2026-09-27T05:38:02.349Z
cosignatures tillnotary-witness/w1, acme-witness/primary
status COMPLETE (witness-cosigned)note <log> prints the checkpoint as a signed note, the text format witnesses and monitors exchange. The note is checked against your pins first; stdout carries it byte for byte and the confirmation goes to stderr, so it can be piped straight on. A note that fails the check is never printed.
$ tilldev notary note acme-releases
notary.tilldev.dev/acme-releases
8
lrZuNuUKOmH8N/CL8R4S0rbqkiBoGDqNJOGuYTuYQC0=
timestamp=2026-09-27T05:38:02.349Z
— tillnotary-log/acme-releases zZiKjXEKTcDk6nN6XIi/5++5OZgWVg7o3XuMU3mJ65zX…
— tillnotary-witness/w1 9crJlQAAAABquKu6Qc1v2HbTxmdeFAlSwXuesvreShyi…
— acme-witness/primary 3fzqpAAAAABquKu67rxRPs+nAZ88HIPwpLNRgpL2v2ip…
verified: log signature and 2 cosignature(s) against your pins (size 8)refusals <log> lists signed witness refusals, each checked against the witness key you pinned. Any entry here is serious; read what a refusal means.
$ tilldev notary refusals acme-releases
! 1 verified refusal(s) on acme-releases: a witness saw history that does not extend what it cosigned.
WITNESS REASON FROM TO OBSERVED
-------------------- ------------------------ -------------- -------------- ------------------------
acme-witness/primary same-size-different-root 2 d89183b0fd9c 2 5fd661a32efb 2026-09-27T05:20:57.000Z
* Keep these statements (--json): each verifies against the witness key, with or without TillNotary.mirror: keep your own verified copy
mirror <log> keeps a full copy of the log on your disk. Each pass downloads the entries added since the last one, hashes them into the log’s Merkle tree, and keeps them only if they rebuild exactly the root the log signed. If the service ever loses, withholds or rewrites an entry, your copy and its signed heads still show what was logged. Anyone who can read the log can run one; public logs need no token.
# a full copy in ./notary-mirror/acme-releases (--dir to choose), kept only if it rebuilds the signed root
$ tilldev notary mirror acme-releases --once
2026-09-27T06:55:07.401Z acme-releases: verified +8 (0 → 8), root 84617e6ac298ca3c…
$ tilldev notary mirror acme-releases --once
2026-09-27T06:55:08.576Z acme-releases: verified +2 (8 → 10), root bc62d10efc84798c…
# re-check the copy with no network
$ tilldev notary mirror acme-releases --offline
OK MIRROR VERIFIED (offline) — 10 entries in notary-mirror/acme-releases rebuild the signed root
origin notary.tilldev.dev/acme-releases
tree size 10
root bc62d10efc84798cb8e5a3ce9d6fa1a14ec73a22b7f06f763b3760fec67987bb
timestamp 2026-09-27T06:55:07.747Z
cosignatures tillnotary-witness/w1, acme-witness/primary
status COMPLETE (witness-cosigned)Without --once it runs every 5 minutes (--interval <sec>, at least 30). A network error or a head the witnesses have not cosigned yet is logged and retried on the next pass. Like checkpoint, it follows witness-complete heads; --latest follows the newest head instead, so the mirror checks each one before any witness has.
| File | Holds |
|---|---|
leaves.jsonl | Every entry in index order, one JSON object per line: leaf_index, leaf, bytes, content_class, submitted_at. |
checkpoints.jsonl | Each signed head the mirror advanced to, with its cosignatures. |
head.json | The newest verified head and what the next pass resumes from. |
alarms.jsonl | Each alarm, with the signed head the mirror held and the one that contradicts it. |
One process writes a directory at a time, and a pass that stops midway keeps nothing it had not verified. If the log contradicts the copy, the mirror stops with exit code 3:
# --latest checks each head as soon as the log signs it, before the witnesses do
$ tilldev notary mirror acme-releases --once --latest
x ALARM: log notary.tilldev.dev/acme-releases REWROTE HISTORY: its signed head at 11 does not extend the one the mirror holds at 10
The log's conflicting signed heads are saved in notary-mirror/acme-releases/alarms.jsonl; keep that file, it is the evidence.
$ echo $?
3alarms.jsonl, and anyone can check the signatures against the log’s public key. Keep the directory as it is, stop trusting new receipts from the log, and escalate, as for a witness refusal.Managing logs
| Command | What it does |
|---|---|
logs | Your logs: size, witness threshold, read access, daily cap, suite, status. |
logs create <slug> [--description <text>] [--suite standard|gov] [--private] [--witnesses <a,b>|none] [--threshold <n>] [--daily-cap <n>] | Create a log. Without --witnesses it gets the platform witnesses; none creates it unwitnessed. --daily-cap defaults to 50,000. |
logs update <slug> [--description <text>] [--public|--private] [--threshold <n>] [--daily-cap <n>] | Change the description, read access, cosignature threshold or daily cap (1 to 50,000 new entries per rolling 24 hours). The threshold cannot exceed the active witnesses. |
logs freeze <slug> --yes | Seal a log for good. Proofs keep working; new entries are refused. Asks for --yes. |
$ tilldev notary logs create acme-releases --description "Release artifact digests" --daily-cap 5000
OK Created notary.tilldev.dev/acme-releases
witnesses tillnotary-witness/w1
required 1
reads public
daily cap 5,000
log key fp 1388 3e7f fc10 01f9 32c0 e322 13f6 aba7
* Next: tilldev notary trust-init acme-releases
$ tilldev notary logs update acme-releases --threshold 2
OK Updated acme-releases
LOG SIZE WITNESSES READS DAILY CAP SUITE STATUS
------------- ---- ---------- ------ --------- -------- ------
acme-releases 0 2 required public 5,000 standard active
$ tilldev notary logs freeze acme-releases
x Freezing is permanent. Re-run with --yes to confirm.
$ tilldev notary logs freeze acme-releases --yes
OK Froze acme-releases at size 2
final root 5fd661a32efbac3704b6b69b673da4f6b5dbace45764566508159be89e4ebaee
signed at 2026-09-27T05:20:57.159ZService tokens
tnot_ tokens are minted with your TillDev sign-in (tilldev login) or a platform API key in TILLDEV_TOKEN with the notary.tokens.write scope; a service token cannot mint another. Owners and admins can mint and revoke.
| Command | What it does |
|---|---|
tokens | Every token: name, prefix, scope, log, last use, expiry and status. Never the secret. |
tokens create --name <n> --scope read|submit|admin [--log <slug>] [--expires <days>] [--quiet] | Mint a token. --log pins it to one log; --expires takes 1 to 3650 days. The token is shown once; --quiet prints only the token, for piping. |
tokens revoke <token-id> | Immediate: the next request with it gets 401. Entries it already submitted stay in the log. |
$ tilldev notary tokens create --name ci-submitter --scope submit --log acme-releases --expires 90
OK Created submit token ci-submitter pinned to acme-releases
id 5d0f3a6e-8c1b-4f27-9e4a-2b7c6d1e0f93
expires 2026-12-26T05:30:00.000Z
token tnot_7c1e4b9d2f8a61c3e5077d2a9b4f1e6c
* Copy the token now; it is shown once. Hand it over as TILLDEV_NOTARY_TOKEN.
$ tilldev notary tokens
NAME PREFIX SCOPE LOG LAST USED EXPIRES STATUS ID
------------ --------- ------ ------------- --------- ---------- ------- ------------------------------------
ci-submitter tnot_7c1e submit acme-releases 59m ago 2026-12-26 active 5d0f3a6e-8c1b-4f27-9e4a-2b7c6d1e0f93
auditor tnot_b40a read all logs - never active 9a2e7b41-3c5d-4e6f-8a1b-0c9d8e7f6a52
old-pipeline tnot_e913 submit acme-releases 26d ago never revoked 1b3c5d7e-9f0a-4b2c-8d4e-6f8a0b2c4d6e
$ tilldev notary tokens revoke 5d0f3a6e-8c1b-4f27-9e4a-2b7c6d1e0f93
OK Revoked token 5d0f3a6e-8c1b-4f27-9e4a-2b7c6d1e0f93 at 2026-09-27T06:02:44.000Z
# in CI: mint straight into a secret store; the token never reaches the terminal
$ tilldev notary tokens create --name release-bot --scope submit --log acme-releases --quiet | gh secret set TILLDEV_NOTARY_TOKENManaging witnesses
| Command | What it does |
|---|---|
witnesses | Witnesses you can attach: the platform’s and your own. |
witnesses register --key-file <f> [--custody-note <text>] | Register a witness key made with witness-node keygen. For a key made elsewhere: --name <n> --suite <id> --public-key <b64>. |
witnesses retire | restore <name> | Stop or resume a witness. Retired keys still verify the cosignatures they made. |
attach <log> <witness> | Add a witness to a log. Re-run trust-init to pin it. |
detach <log> <witness> | Remove a witness. Refused while it would leave fewer active witnesses than the threshold. |
$ tilldev notary witnesses
WITNESS RUN BY ACTIVE SUITE KEY FP CUSTODY
--------------------- -------- ------ -------- ------------------- ------------------------
acme-witness/primary you yes standard d3ee 0e5f a5f0 f399 Acme security team
tillnotary-witness/w1 platform yes standard 29ac d643 062e 5a23 TillDev platform witness
$ tilldev notary attach acme-releases acme-witness/primary
OK Attached acme-witness/primary to acme-releases
* Re-run trust-init acme-releases to pin the new witness, then raise --threshold once it cosigns.
$ tilldev notary detach acme-releases tillnotary-witness/w1
x notary: Detaching tillnotary-witness/w1 would leave 1 active witness for a threshold of 2. Lower witness_threshold first.Retiring a witness that a log depends on is allowed, because a key you no longer trust should stop signing at once, but the CLI names every log that can no longer reach its threshold:
$ tilldev notary witnesses retire acme-witness/primary
OK Retired acme-witness/primary
! acme-releases needs 2 cosignature(s) but has 1 active witness(es): its checkpoints cannot complete.
* Attach a replacement (tilldev notary attach <log> <witness>) or lower --threshold with tilldev notary logs update.Running a witness node
witness-node turns the CLI into a witness you run: keygen writes the key (mode 0600, never overwritten), identity prints what the dashboard needs to register it, and run cosigns every log it is attached to, every 5 minutes by default.
$ tilldev notary witness-node keygen --name acme-witness/primary --key-file witness.key
OK Wrote witness.key (mode 0600). Back it up; it never leaves this machine.
name acme-witness/primary
suite sig-ed25519-mldsa65-sha256.v1
key fp d3ee 0e5f a5f0 f399 a298 2f58 3ad7 134b
$ tilldev notary witnesses register --key-file witness.key --custody-note "Acme security team"
OK Registered acme-witness/primary (fp d3ee 0e5f a5f0 f399)
* Next: tilldev notary attach <log> acme-witness/primary
$ tilldev notary witness-node run --key-file witness.key --state witness-state.jsonrun [--interval <sec> | --once] [--only-pinned] [--state <f>] [--key-file <f>]. With --once it exits 3 if it refused or raised an alarm, so a scheduler can page you. Setup, systemd, rotation and alarms are covered in Witnesses → run your own.
JSON, exit codes & retries
--json prints the verified result instead of the human summary. For verify it carries the checkpoint and a ready-made evidence bundle (signatures shortened here):
$ tilldev notary verify acme-releases 5 --json
{
"leafIndex": 5,
"leaf": "c53d57898e94df8d3fd05e41456bb17357992da9c36bab87edda8fe75f9acafb",
"bytesB64": null,
"contentClass": "hash-only",
"checkpoint": {
"origin": "notary.tilldev.dev/acme-releases",
"treeSize": 8,
"rootHash": "96b66e36e50a3a61fc37f08bf11e12d2b6ea922068183a8d24e1ae613b98402d",
"timestamp": "2026-09-27T05:38:02.349Z",
"suite": "sig-ed25519-mldsa65-sha256.v1",
"cosignatures": [
{ "witness": "tillnotary-witness/w1", "observedAt": 1790487482, "signatureB64": "Qc1v2HbT…" },
{ "witness": "acme-witness/primary", "observedAt": 1790487482, "signatureB64": "7rxRPs+n…" }
],
"complete": true,
"signatureB64": "cQpNwOTq…",
"keyName": "tillnotary-log/acme-releases"
},
"evidence": { "tillnotary_evidence": 1, "origin": "notary.tilldev.dev/acme-releases", … }
}| Exit | Meaning |
|---|---|
0 | Success; everything printed was verified. |
1 | Failure: verification, a refused request, bad input, or the service unreachable after retries. lookup: the log has no entry with that value. |
3 | mirror: the log contradicted the copy. witness-node run --once: the witness refused to cosign or raised an alarm. |
Reads and submissions are retried on network errors and on 429, 502, 503 and 504, up to three times with backoff, waiting as long as the service’s Retry-After asks (up to 30 seconds). A line on stderr says why the CLI is waiting. Admin changes are never retried automatically. Limits are listed in Service tokens → limits.
$ tilldev notary logs
busy; retry 1 in 1s
LOG SIZE WITNESSES READS SUITE STATUS
…Prefer code over shell? The Node SDK does the same with the same verification.