Browse documentation

Portable .tine files default to v5 (since 0.42). --for-legacy writes v4. The .tine/ repository format is v3. tine evidence export writes v4. Formats and protocols.

Docs target stable release v0.44.0.Stable vs main checked 2026-11-07

Reference · v0.44.0

tine cost

From the pinned CLI source. Read upstream.

Current format policy: Portable .tine files default to v5 (since 0.42). --for-legacy writes v4. The .tine/ repository format is v3. tine evidence export writes v4.

CLI spellings

Since 0.16 related verbs are reachable under a noun group. A group spelling is rewritten to the old verb before parsing, so both take the same flags and give the same output and exit code. The old spellings keep working unchanged.

Old spellingNew spelling
tine inittine repo init
tine configtine repo config
tine worktreetine repo worktree
tine branchtine repo branch
tine resettine repo reset
tine reflogtine repo reflog
tine archivetine repo archive
tine packtine repo pack
tine migratetine repo migrate
tine servetine repo serve
tine dutine repo storage du
tine gctine repo storage gc
tine repacktine repo storage repack
tine prunetine repo storage prune
tine compresstine repo storage compress
tine fscktine repo storage fsck
tine reindextine repo storage reindex
tine hooktine repo integrate hook
tine enabletine repo integrate enable
tine disabletine repo integrate disable
tine plugintine repo integrate plugin
tine harnessestine repo integrate harnesses
tine gittine repo integrate git
tine pushtine share push
tine fetchtine share fetch
tine pulltine share pull
tine clonetine share clone
tine remotetine share remote
tine bundletine share bundle
tine archivetine share archive
tine signtine trust sign
tine keygentine trust keygen
tine attesttine trust attest
tine evaluatetine trust evaluate
tine promotetine trust promote
tine verify-tagtine trust verify-tag
tine whotine agent who
tine actionstine agent actions
tine touchedtine agent touched
tine blametine agent blame
tine audittine agent audit
tine migrate-v3tine repo migrate --to-v3
tine bundle verify FILEtine verify FILE (FILE is a bundle, no other arguments)

Since 0.17 log and show also front the read-only verbs. The folded flag is rewritten to the old verb before parsing, so both spellings give the same output and exit code; a flag the old verb does not take is a usage error (exit 2), as are --cost with --price, --at without --price, and log --search with a log option (--graph, --all, --oneline, --limit, --commit, --pr, REF).

Old spellingNew spelling
tine search QUERYtine log --search QUERY
tine lstine log --legacy
tine ls --tag T --model M ...tine log --tag T --model M ... (an ls filter implies --legacy)
tine cost RUNtine show RUN --cost
tine price RUN [--at DATE]tine show RUN --price [--at DATE]

tine log without these flags is unchanged: it reads the v3 repository, while --legacy lists the legacy .tine_runs index that tine ls lists.

tine stats --repo PATH is new in 0.36: run inside a v3 repository (or with --repo), stats counts the repository's store (runs, events, blobs, objects, canonical bytes, cost; --json adds a store object and scope: "v3-repository") instead of the legacy .tine_runs index. --group-by takes model, status or day there; the ls filters and --deep stay legacy-only. Elsewhere stats reads the legacy index exactly as before.

tine trust policy set|list|show|rm|default is new in 0.16 (no older spelling): named trust policies for tine verify --policy NAME (see below).

tine agent id|show|trust|keys are unchanged. tine GROUP --help lists a group; tine help --all lists every command.

tine hold add|list|release OID|REF is new in 0.35 (no older spelling): it keeps objects and everything they reach from prune, gc --prune=now, rewind --apply and remote deletes. Also new in 0.35: gc --prune=never|now (default never until 0.39, which adds retention and makes it the default: see tine retention show|check and REPOSITORY.md), verify --offline --hermetic (exit 6 where no OS sandbox exists, Windows included), rewind --allow-control-dirs, replay --trust-recorded-prompt and serve reseal --to aes4 --from aes2.

New flags in 0.37 (no new subcommands):

  • gc --level N / repack --level N: the pack compression level (zstd 1-22, default 19; zlib caps at 9). Auto-pack reads "auto_pack": {"level": N} from the repository's config.json.
  • archive create --archival: writes the archive with xz (storage codec 4, zlib where Python lacks lzma), so a stock Python 3.11+ restores and verifies it with no optional package. opentine 0.36 and older cannot read codec 4, which is written only on this request.
  • fsck --budget SECONDS / fsck --max-objects N: stop the object walk cleanly; the JSON report gains checked and complete (exit 0 when nothing checked failed), and stderr says how far it got.
  • gc also removes staging files left by killed writers once they are an hour old (reaped_temp in --json); live writers' files are never touched.

New in 0.38: tine rewind restores a workspace snapshot (a tree blob, SPEC §1.5.7) exactly. tine rewind TREE_OID takes the tree id directly; tine rewind RUN[@STEP] --tree uses the tree linked from that step (outputs.tree, or the nearest earlier step with one). --repo DIR names the repository holding the snapshot (default: the one containing --root). Dry run by default; --apply writes every file, link and executable bit back byte for byte and removes tracked paths the snapshot did not have, so changes made through a shell are undone too. Writes go through the 0.35 O_NOFOLLOW directory-descriptor writer; control files are refused without --allow-control-dirs; ignored, version-control and omitted paths (too large or unreadable at capture) are left alone; a file stored redacted is refused. --json reports restore, remove, unchanged, refused and kept.

Errors, --json and exit codes (0.36)

Every leaf subcommand accepts --json. Commands with their own JSON payload keep it unchanged; fleet up (with or without --dry-run) and replay --dry-run/--inspect now emit one JSON object instead of ignoring the flag. Any other command wraps its human output:

{"command": "fsck", "exit_code": 0, "output": "..."}

The v3 plumbing verbs (fsck, object, migrate-v3, fetch, push) always print bare JSON; --json on them is a no-op. output.format = json turns on only a command's own JSON, never the wrapper.

Errors go to stderr. Under --json, stdout carries only JSON: a failing command that wrote no JSON of its own emits one error object (a usage error included) and anything human it printed moves to stderr:

{"error": {"code": "not_found", "exit_code": 3, "message": "..."}}
ExitcodeMeaning
0—ok
1errorany other failure
2usagebad flags or arguments, or a refused flag combination
3not_founda run, ref or object that does not exist
4verification_faileda signature, digest or verdict did not hold
5budget_exhausteda budget or limit stopped the work
6trust_requiredneeds a trust decision, or is unsupported here (verify --hermetic)

Existing exit statuses keep their meaning; commands move to the specific codes as they are revised, so a script should treat any non-zero status as failure and read error.code for the reason.

New in 0.36: serve --oidc-issuer URL --oidc-audience AUD verifies OIDC bearer tokens instead of the static token (--oidc-jwks-uri URL pins the key set, otherwise it comes from the issuer's discovery document; every URL must be https:// and none is ever taken from a token; --oidc-tenant-claim and --oidc-roles-claim name the claims). The static token must now hold at least 32 bytes; serve --allow-weak-tokens starts with a shorter one while it is rotated. After 5 consecutive failed authentications a client gets 429 with Retry-After for a delay that doubles per failure (up to 5 minutes), and a 500 answers only {"error":"internal","request_id":...}, logging the exception.

New in 0.44: serve --quota-rate PER_SECOND [--quota-burst N] gives every (tenant, subject) a token bucket; an upload or ref update over quota gets 429 with Retry-After. At most --quota-max-keys buckets (10,000) are held, the least recently used evicted first. Clients retry a 429 at most 3 times, honouring Retry-After up to 30 seconds and within the request deadline; a 0.34 client reports remote returned HTTP 429 and stops. serve --vsa-dir DIR writes an in-toto SLSA Verification Summary Attestation (https://slsa.dev/verification_summary/v1) for every admitted push to DIR/<tenant>/<sha256>.json.

Default output format

tine config set output.format json    # every command with --json now emits JSON
tine config get output.format         # prints json; exit 1 when unset
tine config unset output.format
OPENTINE_OUTPUT=text tine show RUN    # the env var wins over the file
tine show RUN --text                  # human output for this call only

output.format is text or json, stored only in the user's $XDG_CONFIG_HOME/opentine/settings.json (written 0600; a repository cannot set it). Precedence: an explicit --json or --text on the command, then OPENTINE_OUTPUT, then the file, then text. Every command that has --json also has --text; giving both is an error. Commands whose --json is only the 0.36 wrapper ignore the setting and never read the file. A value other than text/json in the env var or the file, or a malformed file, stops the command with a one-line error (exit

  1. instead of guessing. tine config set|get|unset are the user settings; tine config KEY [VALUE] still edits the repository's porcelain keys. With no setting, every command's output is unchanged.

Op log and undo (0.36)

Every tine command that moves a local ref (branch, tag, reset, fork, pull, rebase, cherry-pick, ...) appends one entry to the op log: op id, command, actor, time and each ref's old -> new. The log is opentine-log/1, a hash-chained record log in the C2SP tlog-tiles layout under .tine/log/op/ (checkpoint, tile/<L>/<N>, tile/entries/<N>), with an Ed25519 signed-note checkpoint; the verifier key is in .tine/log/op/vkey.

  • tine op log [-n N] [--json] lists ops, newest first.
  • tine op verify [--vkey KEY] [--json] recomputes every tile, the Merkle root, the hash chain and the checkpoint signature from local files only (it never touches the network). --vkey pins a key obtained out of band.
  • tine undo [OP] [--force] [--json] puts back the refs OP (id, id prefix or index; default the latest) moved, as a new forward ref transaction that is itself logged (undoes), so tine undo twice redoes. It refuses (exit 1) when a ref was moved since by a later op (superseded) or by a writer that does not log, such as opentine 0.34 or a library caller (gap: the ref differs from the logged value, or its reflog has newer rows); --force undoes anyway.

Offline verification (0.25)

tine verify RUN --offline                        # human report
tine verify RUN --offline --json --report r.json # tine-verify-report/2
tine verify RUN --offline --pubkey release.pub --strict
tine verify RUN --offline --attestation a.sigstore.json --trusted-root trusted_root.json \
    --cert-identity ci@example.com --cert-oidc-issuer https://token.actions.githubusercontent.com
tine verify RUN --offline --action-policy policy.toml

Every check runs from local files only; a socket connect or DNS lookup during the run is a hard error. Each check names its trust basis.

What "offline verified" proves:

  • the file's bytes match its own sha256 digest (internal consistency: anyone who edits a run can also recompute its digest, so on its own this proves nothing about who wrote it);
  • with --pubkey, --pin, --key-file or --key-env, that the holder of that key signed these bytes -- the only case the report fills authorship;
  • with --attestation, that the statement is signed and names this digest, and that a receipt it carries is signed by the Rekor key or trusted root you gave;
  • for a keyless bundle, that the certificate chains to your Fulcio root / trusted root, matches --cert-identity/--cert-oidc-issuer, and (with CT keys) carries a valid SCT.

What it does not prove: that the log still contains the entry or was not rolled back (no live inclusion or consistency check), that a key or certificate was not revoked later, that your trusted root is current, or anything about a check reported unavailable-offline. --trust-embedded-key verifies a key the file itself carries, so it is reported as trust-on-first-use, never authorship. Use --strict to make any missing required check fail the verdict.

--locate reports where a failed run broke, using only finer-grained hashes that exist: step ids (each covers a step's kind, inputs, outputs, model_info, tool_info, error and parent_ids, so it names the step, not the field), a stored action manifest, or a key-verified attestation subject. When none of them explains the break it says locate.unlocalisable rather than guess. Byte offsets index the canonical JSON of the redacted, stored body with metadata removed (what the digest covers), not the file on disk.

tine verify --offline DIR|REPO writes one tine-verify-dataset/1 report with a Merkle root over the sorted (run_id, digest) pairs (leaf sha256(0x00 || canonical_json([run_id, digest])), node sha256(0x01 || left || right), an odd node carried up unchanged). A repository run's digest is its tine-actions/1 run_digest.

tine timestamp data RUN
tine timestamp attach RUN TOKEN.tsr
tine verify RUN --offline --tsa-roots FILE.pem

0.31.0: offline RFC 3161 timestamp checks. timestamp data prints the exact bytes a token must cover (the run's digest string, no newline); build the query and fetch the response yourself, for example tine timestamp data run.tine > bound.txt, openssl ts -query -data bound.txt -sha256 -cert -out q.tsq, then POST q.tsq to your TSA. opentine never contacts a TSA. timestamp attach checks the response parses and covers the run's current digest, then stores it in RUN.timestamps.json. verify --offline then reports one timestamp check per token (imprint, TSA signature, timestamping EKU, genTime within the certificate's validity, chain to a root in --tsa-roots or the --trusted-root TSAs) and a timestamps section with genTime, TSA name, policy OID and hash algorithm (SPEC §8.6).

tine timestamp data RUN --signature [N]
tine verify RUN --offline --report r.json --sign-key KEY [--timestamp]
tine verify-report data r.json
tine verify-report attach r.json TOKEN.tsr
tine verify-report check r.json [--pubkey P] [--tsa-roots FILE.pem] [--allow-unsigned] [--json]

0.39.0 (X20, X22). timestamp data --signature prints sig:sha256:<hex> of the run's N-th signature block; a token over it verifies only while the run carries that signature, and the report's signing_time is its genTime (the verified signing time). Root-bound tokens keep verifying. --sign-key (an Ed25519 key file from tine keygen, --report only) writes the tine-verify-report/2 as signed canonical JSON; --timestamp (needs --sign-key) also writes r.json.tsdata, the bytes a TSA token over that signature must cover. verify-report check verifies the report offline and exits 0 when it is authentic (whatever verdict it records), 4 when its signature or a token fails, and 4 for a /1 or unsigned report unless --allow-unsigned (SPEC §8.7).

tine replay RUN --verify --attest OUT.intoto.jsonl --attest-key KEY

Writes the verdict as an in-toto replay/v1 DSSE envelope, predicate type https://opentine.dev/attestation/replay/v1 (SPEC §8.8). Both flags need --verify; --attest needs --attest-key.

tine custody record ACTION SUBJECT --sign-key KEY [--actor A] [--note N] [--repo R]
tine custody record timestamp SIG_BOUND --token T.tsr --sign-key KEY
tine custody log [--json]
tine custody verify [--pubkey P] [--vkey V] [--tsa-roots FILE.pem] [--json]

Chain of custody on the repository's opentine-log/1 (.tine/log/custody): each record says who (--actor, default the login name) received, exported, verified, transferred or stored what, when, and is signed by that actor's Ed25519 key. A timestamp record stores a TSA token over an earlier record's sig_bound (printed by record and log). verify checks the checkpoint, tiles, hash chain, signatures and tokens offline and exits 4 on any break (SPEC §8.9).

tine witness vkey --key KEY --name NAME
tine witness cosign CHECKPOINT --log-vkey V --key KEY --name NAME
                    [--proof P.json | --tiles DIR] [--state DIR] [--out FILE] [--json]
tine verify TARGET --offline --witnesses N-of-M --witness-vkey V... [--log-vkey V...]
tine bundle create FILE REF... --transparency

0.44 (X33). witness cosign checks a signed-note checkpoint against the pinned log key and against the last checkpoint this witness cosigned for that origin (--state, default $XDG_STATE_HOME/opentine/witness): the same size must have the same root, a smaller size is a rollback and a larger one needs an RFC 9162 consistency proof (--proof, or computed from a local tile mirror with --tiles). Only then does it print the checkpoint with a C2SP cosignature/v1 line added; a fork or rollback exits 4. witness vkey prints the key others pin with --witness-vkey. verify --offline --witnesses N-of-M takes a bundle, an opentine-tlog-receipt/1 file or a bare checkpoint (with one --log-vkey) and exits 4 unless N of the M pinned witnesses cosigned every checkpoint; --log-vkey pins a log key by origin (otherwise the receipt's key is used, trust on first use). bundle create --transparency embeds op-log and push receipts for the bundled refs so they verify from the bundle alone; such bundles need opentine 0.44+ to read. tine push and tine fetch check the remote's audit log automatically (SPEC §8.10).

tine scitt verify STATEMENT --key PEM [--receipt R]... [--service-key PEM] --experimental

0.44, experimental: a COSE_Sign1 SCITT signed statement and its RFC9162_SHA256 receipts, verified offline against pinned keys (SPEC §8.12). Without --experimental it exits 2. Interop with scitt-api-emulator is not claimed.

tine evidence export RUN... --out DIR [--sign KEY] [--pubkey P] [--pin FPR]
                     [--action-policy FILE] [--attestation FILE]
                     [--trusted-root FILE] [--operator NAME]

Writes MANIFEST.sha256, custody.jsonl, timeline.jsonl, timeline.csv, verify-report.json, trust/ and runs/. custody.jsonl is outside the manifest (it holds the only timestamp) and names the manifest's SHA-256; with --sign it is signed with Ed25519 over opentine.custody.v1: + canonical JSON. --attestation (one run only) and --trusted-root (only with a keyless attestation) feed the attestation checks and are copied into trust/. docs/conformance/minimal_verifier.py evidence DIR re-checks the folder without opentine (SPEC Part 8).

tine evidence report RUN --format html|pdf -o PATH [--pubkey P] [--pin FPR]
                     [--action-policy FILE]

New in 0.39. Writes one run's offline verification, timeline and the canonical evidence JSON (embedded as <script type="application/json" id="tine-evidence">) as one self-contained page. The HTML is canonical and byte-identical across runs: no wall-clock time (only the run's own), the source reduced to its file name, sorted-key canonical JSON, inline CSS, the tine view CSP (no scripts, nothing fetched), and every string re-redacted. --format pdf needs pip install 'opentine[pdf]' (pikepdf; exit 6 without it) and is a derivative: fixed /CreationDate from the run, content-derived /ID, evidence.json attached with /AFRelationship /Data. It is deterministic for a given pikepdf/qpdf version, but only the HTML is gated on byte identity; the PDF uses the base-14 Courier font (no system font is read), so it is not yet PDF/A-3 conformant.

tine retention show|check [--repo DIR] [--json] (0.39) prints the retention policy, or dry-runs what default gc would prune.

Trust policies

tine trust policy set prod --pubkey ~/keys/release.pub --rekor-key ~/keys/rekor.pem --require-receipt
tine trust policy set ci --cert-identity ci@example.com --cert-oidc-issuer https://token.actions.githubusercontent.com \
    --fulcio-root ~/keys/fulcio.pem --rekor-key ~/keys/rekor.pem --ct-key ~/keys/ctlog.pem --require-sct
tine trust policy set sigstore --cert-identity ci@example.com \
    --cert-oidc-issuer https://token.actions.githubusercontent.com --trusted-root ~/keys/trusted_root.json
tine trust policy list [--json]
tine trust policy show prod [--json]
tine trust policy default prod        # or: --clear; no argument prints it
tine trust policy rm ci
tine verify RUN --attestation a.intoto.jsonl --policy prod
tine verify RUN --no-policy           # ignore trust.default for this call

A policy holds pubkey, pin, rekor_key, require_receipt, cert_identity, cert_oidc_issuer, fulcio_root, and (0.18) ct_key (a list of CT log key files), require_sct and trusted_root (which replaces rekor_key, fulcio_root and ct_key). Flags given on the command line win over the policy; any key flag (--pubkey, --pin, --key-env, --key-file, --trust-embedded-key) replaces the policy's key as a whole. Fields that do not apply (keyless fields for a key-signed attestation, receipt fields for a plain run verify) are unused rather than refused. The default policy applies only when no key, receipt or keyless flag and no --policy is given. --trust-embedded-key cannot be stored in a policy.

Sigstore trusted root (0.18)

tine verify RUN --attestation a.sigstore.json --cert-identity ci@example.com \
    --cert-oidc-issuer https://token.actions.githubusercontent.com --trusted-root trusted_root.json
tine trust root update --root root.json [--repo-url https://tuf-repo-cdn.sigstore.dev] [--json]
tine trust root update                # later runs: stored root and repository

--trusted-root FILE takes the Rekor keys (tlogs), Fulcio chains (certificateAuthorities) and CT log keys (ctlogs) from one Sigstore trusted_root.json, honouring each entry's validFor window. It is a usage error beside --rekor-key, --fulcio-root or --ct-key. When the root lists CT logs, an embedded SCT is required.

tine trust root update is a minimal TUF client: it refreshes trusted_root.json into $XDG_CONFIG_HOME/opentine/sigstore/, after which that copy is the default for keyless tine verify --attestation calls that name no key file. Nothing is ever fetched implicitly. No TUF root is embedded: the first run needs --root FILE, an initial root.json you obtained and checked out of band (for the Sigstore public-good instance, from sigstore/root-signing, e.g. its metadata/root_history/ or https://tuf-repo-cdn.sigstore.dev/N.root.json compared against the signing ceremony). To start over, remove $XDG_CONFIG_HOME/opentine/sigstore/tuf/.

Agent Trace export (0.19)

tine export RUN --format agent-trace                    # record on stdout
tine export RUN --format agent-trace --output trace.json

Writes one Agent Trace record (RFC 0.1.0, validated against https://agent-trace.dev/schemas/v1/trace-record.json). Each file the run's successful tool calls wrote is a files entry with one conversation per model, contributor ai, model_id spelled provider/model when the provider was recorded. Line ranges are emitted only when they are known: a whole-file write that is the run's last write to that path covers lines 1..N. Edits, patches and shell redirects get file-level attribution (an empty ranges array). Absolute paths are made relative to the enclosing repository (the nearest directory with .git, else the current directory); paths outside it are left out and counted. vcs and conversation url are omitted, since a run records neither; the run id rides in metadata["io.github.0xcircuitbreaker.opentine"]. version is "0.1", the spelling the published schema accepts. --endpoint is refused.

Citation export (0.25)

tine export RUN --format citation --output CITATION.cff
tine export RUN --format ro-crate --output ro-crate-metadata.json

citation writes a Citation File Format 1.2.0 document (type: dataset); ro-crate writes an RO-Crate 1.1 ro-crate-metadata.json with the metadata descriptor, a root Dataset, the run file, and opentine plus each model as SoftwareApplication entities. Both carry the run id, the integrity digest (sha256:…, SPEC §3.5), every model the run recorded, the run's own creation time and the opentine version. Nothing else varies (no clock, host or path), so the same run and version always give the same bytes. A run whose digest does not verify is refused, and --endpoint is refused. Without --output the document goes to stdout.

Entire checkpoint import (0.19)

tine import . --format entire --save run.tine        # newest checkpoint, last session
tine import .::a3b2c4d5e6f7/0 --format entire --repo runs

Entire commits coding-agent sessions to the entire/checkpoints/v1 branch (<id[:2]>/<id[2:]>/metadata.json, one numbered directory per session with metadata.json, full.jsonl and prompt.txt). SOURCE is the git repository; ::ID picks a checkpoint by id or unique prefix and /N a session (0-based, in the summary's order). The branch is read with git cat-file, never checked out; origin/entire/checkpoints/v1 is used when there is no local branch. --format auto recognizes a directory SOURCE. The session transcript is imported by the Claude Code, Codex or Gemini CLI importer; any other agent imports as one human step carrying its prompt, with an import warning. attributes["entire"] on the first step keeps the checkpoint and session ids, agent, model, strategy, branch, commit, files touched and Entire's token usage. full.jsonl holds the session up to that checkpoint, so the run is the session so far. Session metadata over 1 MiB and transcripts over 64 MiB are refused before they are read.

Per-checkpoint refs, --all and export (0.42)

tine import . --format entire --save run.tine          # refs first, v1 branch as fallback
tine import . --format entire --all --repo runs        # every checkpoint session, one run each
tine export run.tine --format entire --output ~/proj   # write the run as a checkpoint ref

Current entireio/cli keeps one ref per checkpoint, refs/entire/checkpoints/<id[-2:]>/<id>, whose commit tree is the checkpoint (metadata.json, 0/, 1/ ...; sessions listed as /<N>/metadata.json). Those refs are read first, newest commit first; any checkpoint id found only on the entire/checkpoints/v1 branch is read from there. attributes["entire"] ["layout"] says which (refs or branch). --all records each session of each checkpoint on heads/entire/<id, lowercased>/<N> and needs --repo (--save, --ref and --fleet are refused with it); with --json it prints {"command": "import", "format": "entire", "all": true, "runs": [...]}.

export --format entire writes the run, redacted, as one checkpoint in the --output git repository: a 12-hex id derived from the run id, a Claude Code full.jsonl, prompt.txt, content_hash.txt and both metadata files, committed with Entire's Checkpoint: <id> message and Entire-Session/-Strategy/-Agent trailers. Only git objects and that one ref are written. Exporting the same run again is a no-op; a changed run becomes a child commit on the same ref.

Cline and Roo Code task import (0.42)

tine import ~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/tasks/1730985600000 --save run.tine
tine import --from-default-location --format roo-task          # list Roo Code tasks

SOURCE is a task directory (or its api_conversation_history.json); ui_messages.json and Cline's task_metadata.json beside it are read when present. Each assistant message is a model step, tool results (native tool_result blocks or the [tool for 'x'] Result: text convention) are tool steps, and other user turns human steps, with <environment_details> blocks dropped. Token usage comes from the matching api_req_started record (Roo's apiProtocol: "openai" counts cache reads inside tokensIn), else from the message's metrics. auto tells Roo from Cline by the rooveterinaryinc.roo-cline folder in the path. Windsurf is not supported: it has no documented local session format.

Writer format (0.26, 0.42)

Since 0.42 .tine files are written as format v5 (SPEC Part 10): the v4 document with UTF-8 text blobs spelled as text, carrying the same oids, root and tine-sig/3 signatures; opentine 0.40 or newer reads it. --for-legacy writes v4 for older readers, and tine evidence export always copies its runs as v4. Since 0.26 every writer produces format v4 objects (SPEC Part 9): .tine files were tine-artifact/4 until 0.42, signatures tine-sig/3, Ed25519 attestations on a run tine-attest/2, and tine init creates an object-format-4 repository (an existing repository is marked object format 4 before the first object 0.26 writes into it). Reading never changes: every older file and repository stays readable, and a read never rewrites it.

SpellingWrites
(default)format v5 .tine files (SPEC Part 10; opentine >= 0.40 reads); new repositories object format 4
tine --for-legacy COMMAND ... (or --format 4)format v4 .tine files, for readers before 0.40, for this command only
tine --format 2 COMMAND ...the deprecated v2 file / object-format-3 repository writers, for this command only
OPENTINE_FORMAT=2 (or 3)the same, for the whole environment
OPENTINE_FORMAT=4v4 files for the whole environment, strict: no deprecated block even where v4 cannot express it (an HMAC attestation, sign_artifact on a v2 dict)
tine migrate FILEupgrades to v4 (--to v4 is the default; -o/--save names the output, else NAME.v4.tine)
tine migrate FILE --to 2the deprecated in-file v1 -> v2 migration

The legacy writers print one deprecation warning per process on stderr. A file or repository written by 0.26 needs opentine 0.26 or newer; share with older releases with tine --format 2. An explicit write refuses an integer beyond 2**53-1 or a non-finite number, naming the field; recorders (tine run, tine record, imports, hooks, serve --otlp) store such a value as its exact string and list the field under metadata.warnings instead of losing the run. tine bundle (and bundle push/fetch) carries the tine-sig/3 blocks of imported v4 runs in the bundle manifest.

FIPS mode (0.41)

tine --fips COMMAND ...          # or OPENTINE_FIPS=1

--fips is global, like --format. Without an active OpenSSL FIPS provider in cryptography every command fails before it runs with exit 6 (trust_required) and, under --json, {"error": {"code": "trust_required", ...}}. A non-approved primitive met later (ChaCha20-Poly1305, writing the archival xz form) fails the same way. tine verify --offline --json adds crypto: {mode, provider_fips, openssl} to the report.

tine serve --root R --tenant T reseal --to aes6 rewrites a tenant's objects as TINEAES6 (key id, epoch, XAES-256-GCM); it needs TINE_KMS_KEY_ID set.

Fan-out status and MCP tools (0.36)

tine fleet status [ID] --repo PATH lists the repository's tine fan-out records (.tine/fanouts/) instead of the fleets in --dir; with --json it prints {"command": "fanout status", "fanouts": [...]}.

The MCP server (python -m opentine.mcp_server) builds its tool list from one registry (opentine/_mcp_registry.py), each tool annotated read or write. The default exposes only read tools; --allow-writes or OPENTINE_MCP_ALLOW_WRITES=1 adds exactly the annotated write set (never promote_run). The read tools generated from the registry return the same payload as their command's --json:

ToolCommand
verify_runtine verify RUN --offline --json
logtine log [REF] --limit N --repo REPO --json
fleet_statustine fleet status [ID] --json
fanout_statustine fleet status [ID] --repo REPO --json
costtine cost RUN --json
agent_showtine agent show TARGET --repo REPO --json
statstine stats [--group-by KEY] --limit N --json

New in 0.44, recall(query, limit=10) is a hand-written read tool over the derived search index (tine repo-search --rebuild-index, see REPOSITORY.md): runs whose stored text mentions every word of the query, best BM25 match first, at most 50 rows of run id, status, models and a snippet of at most 240 characters. It never scans the store or builds the index; without one it answers {"indexed": false, ...}.

Cost export (0.44)

tine cost RUN --otlp PATH|-                     # OTLP/JSON metrics from the cost aggregate
tine cost RUN --statement PATH --sign-key KEY [--signer NAME]
tine cost RUN --verify-statement PATH [--pubkey KEY.pub]

--otlp writes an ExportMetricsServiceRequest (a /v1/metrics body): gen_ai.client.token.usage by gen_ai.token.type and opentine.cost.usd in total, by gen_ai.request.model and by opentine.step.kind, as cumulative sums on a resource carrying opentine.run.id. --statement writes a tine-cost-statement/1: the aggregate (totals, tokens, by model, by kind) with an Ed25519 tine-sig/3 block under opentine.cost-statement.v1:. --verify-statement recomputes the aggregate from RUN and prints {"ok", "matches_run", "signature", "basis"}, exiting 4 unless both the numbers and the signature check (without --pubkey the embedded key is trusted on first use, and basis says so). The statement covers the cost the run recorded; tine price re-prices from the catalog.

Per-turn capture (0.38)

CommandWhat it does
tine hook capture [--agent NAME] --repo PATHopentine-hook/1: one JSON event on stdin becomes one durable step of the session's run; prints a JSON response and always exits 0
tine enable [--agent NAME] [--dry-run] --repo PATHAdds the capture shims to the Claude Code, Codex and Gemini CLI hook files and sets hooks.capture; --dry-run prints the unified diff only
tine disable [--agent NAME] [--dry-run]Restores each hook file byte for byte (or removes only the shims if it was edited since)

Capture records nothing unless the repository's trusted config sets hooks.capture. See CAPTURE.md.

MCP proxy capture (0.38)

tine mcp-proxy [--save PATH] [--force] [--repo PATH [--ref REF]] -- CMD [ARGS...]
tine mcp-proxy --http-upstream URL [--listen HOST:PORT] [--allow-non-loopback] [--save PATH] [-- CMD...]

Relays an MCP session over stdio (spawning CMD) or Streamable HTTP and records each JSON-RPC exchange as a redacted tool step, saved incrementally. In HTTP mode an optional CMD is run alongside and the proxy stops when it exits; otherwise it serves until Ctrl-C or SIGTERM. --listen defaults to 127.0.0.1:0 (the chosen port is printed on stderr). See CAPTURE.md.

Assurance tiers (0.39)

CommandWhat it does
tine proxy keygen [--force] [--json]Creates the proxy witness key (Ed25519) under $XDG_CONFIG_HOME/opentine/proxy-keys/, apart from every agent key; tine record and tine mcp-proxy sign their finished runs with it (tine-sig/3)
tine verify RUN --min-assurance T0..T3Exits 4 (verification_failed) when the verified tier is below the floor
tine verify RUN --proxy-pubkey FILEAlso trusts this proxy public key as a T2 witness (repeatable)
tine verify RUN --min-assurance T3 --tsa-roots FILE.pemLets an attached RFC 3161 token count toward T3 (outside --offline, --tsa-roots needs a floor)
tine verify RUN --compare-capture OTHER [--json]Divergence report: the better-witnessed run against the other, step by step (MCP tools/call and prompts); exits 4 when they diverge

Tiers: T0 self-reported import, T1 hook-captured, T2 witnessed by the record proxy or tine mcp-proxy and signed with a trusted proxy key, T3 T2 plus a verified timestamp. The tier a capture claims is recorded (metadata.capture.claimed_tier); the tier reported is computed at verify time, and a claim alone never rises above T1. Every report carries an assurance object, {"claimed", "verified", "witness", "reasons"}: tine verify --json, tine verify --offline (and so the MCP verify_run tool) and tine show --json; the human output prints one assurance line.

Key storage and signers (0.41)

CommandWhat it does
tine keygen --out PATH / --agent NAMEWrites the private key as encrypted PKCS#8 (PEM, PBES2/AES-256); --unencrypted writes the deprecated plaintext-hex form
tine key encrypt PATH [--json]Rewrites a plaintext-hex private key as encrypted PKCS#8 in place; an encrypted key is left alone ("changed": false)
tine key show SIGNER [--json]Opens a signer and prints its signer, algorithm, key_id (sha256:<hex>) and public_key
tine proxy keygen --signer URI [--force]Makes the key an agent, HSM or KMS holds the proxy witness key; only the URI and the public half are stored
tine proxy migrate --to URI [--json]Moves the proxy key into a signer; the old pair goes to proxy-keys/retired/ and stays trusted for runs it signed

The passphrase for an encrypted key comes from OPENTINE_KEY_PASSPHRASE, the OS keyring (OPENTINE_KEY_KEYRING=1 with pip install 'opentine[keyring]'), or a prompt when stdin is a terminal; with none, keygen refuses (exit 1) and loading the key fails. A plaintext-hex key still loads, with a PlaintextKeyWarning on stderr.

Every flag that takes an Ed25519 signing key file (sign --ed25519-key-file, custody record --sign-key, verify --offline --report --sign-key, evidence export --sign, replay --attest-key, push --ed25519-key-file, export --format intoto --ed25519-key-file, actions sign) also takes a signer URI:

SignerBackend
file:PATH or PATHA key file (encrypted PKCS#8, or legacy hex)
ssh-agent:FINGERPRINTAn Ed25519 key in ssh-agent (SSH_AUTH_SOCK); SHA256:... as ssh-add -l prints it, sha256:<hex>, or * for the only one
pkcs11:token=T;object=O?module-path=LIBAn HSM via opentine[pkcs11] (CKM_EDDSA); PIN from pin-source=env:VAR, pin-value= or OPENTINE_PKCS11_PIN
kms:aws:ARNAWS KMS ECC_NIST_EDWARDS25519, ED25519_SHA_512 over the raw message (at most 4096 bytes), via opentine[kms-aws]
kms:gcp:RESOURCEGCP Cloud KMS EC_SIGN_ED25519 (.../cryptoKeyVersions/N), via opentine[kms-gcp]

The flag is not spelled --signer there because tine sign --signer already names the signer recorded in the block. Every backend's signature is checked against its public key before it is used.

Key lifecycle and verifier hardening (0.41)

CommandWhat it does
tine key rotate KEY [--out NEW] [--json]A new Ed25519 key, written as encrypted PKCS#8, replaces the private key file KEY (and KEY.pub); the old pair is kept as KEY-retired-<fp12>. The old key gets not_after = now, the new one not_before = now, and the old key signs an endorsement of the new one (tine-key-rotation/1). --out NEW writes the new key elsewhere and leaves KEY in place
tine key rotate --agent NAME / --proxyThe same for an agent key (keygen --agent) or the proxy witness key (proxy keygen); the retired proxy key goes to proxy-keys/retired/, as with proxy migrate, and stays trusted for T2. A proxy key held by a signer is refused (use proxy migrate)
tine key revoke KEY [--at TIME] [--json]Records revoked_at (RFC 3339, default now) for sha256:<hex>, a .pub file or a private key file. An earlier revocation already recorded stands; a malformed --at is a usage error (exit 2)
tine verify RUN --offline --tsa-policy OIDAccept only RFC 3161 tokens issued under this TSA policy (repeatable)
tine verify RUN ... --allow-stale-rootUse the TUF-installed trusted root even though its metadata has expired (a warning on stderr)

Both tine key verbs write only $XDG_CONFIG_HOME/opentine/key-lifecycle.json.

Fleet records and delegation (0.43)

CommandWhat it does
tine key delegate KEY --to PUB --agent NAME --action NAME [--max-depth N] [--not-before T] [--not-after T | --days N] [--parent CHAIN] --out CHAIN [--json]KEY signs an opentine-delegation/1 certificate (SPEC 4.10) giving PUB the named agents and tools (* is any) for the window (default now to now + 30 days); --out gets the chain, root first, with --parent's certificates before the new one. --agent and --action repeat
tine key delegation show CHAIN [--json]Each certificate's digest, issuer, subject, window, parent and scope
tine key delegation verify CHAIN --signer PUB|FPR [--at T] [--agent NAME] [--action NAME] [--root FPR] [--json]Verifies the chain offline for that signer at T (default now); exit 4 and the delegation.* code (expired, scope-violation, broken-chain, wrong-signer, bad-signature, too-deep, malformed) when it fails
tine fleet record RUN.tine [--json]The run's opentine-fleet/1 record (SPEC 4.11), captured from OPENTINE_FLEET_ID/_MEMBER/_ROLE/_PARENT, and the length of its delegation chain
tine fleet record RUN.tine --sign-key KEY --out FILEWrites an opentine-fleet-record/1 document: the record bound to the run's id and digest under a tine-sig/3 block
tine fleet record RUN.tine --attest OUT --attest-key KEYWrites an in-toto https://opentine.dev/attestation/fleet/v1 DSSE envelope whose subject is the run's digest
tine fleet record FILE [--pubkey PUB]On a signed fleet record: verifies it offline (exit 4 on failure); without --pubkey the embedded key is trusted on first use

A capture with OPENTINE_DELEGATION=CHAIN stores the chain in the run; tine verify RUN --offline then adds a required delegation check: the chain must verify for the run's signer at its signing time, its agent (agt_ ref, harness, fleet member or role) and every tool it used. The check's detail lists the chain root first; its trust basis says whether the root is one of --pin. A trusted .tine/agents.toml may also carry windows ([agents.NAME.lifecycle."sha256:<hex>"] not_after = "..."), as may a trust policy's key_lifecycle table; the strictest window wins.

tine verify RUN --offline adds a key-lifecycle check whenever a window names the signer key, judged against the report's verified signing_time (a signature-bound RFC 3161 token): lifecycle.valid, lifecycle.expired, lifecycle.not-yet-valid or lifecycle.revoked. With no verified time it is unavailable-offline / lifecycle.unverified-time, except that a revoked key fails (lifecycle.revoked-unverified-time) and a claimed signed_at outside the window fails. Plain tine verify RUN --pubkey/--pin has no verified time: it exits 4 (verification_failed) on those failures and otherwise notes that the window was checked against an unverified time.

Timestamp tokens now fail with timestamp.signing-cert-mismatch (no single ESS signingCertificate/V2 attribute binding the TSA certificate), timestamp.critical-extension, timestamp.policy-not-allowed, or timestamp.missing-eku when the timeStamping EKU is not critical or not alone; genTime plus or minus the token's accuracy must lie inside the TSA validity. An attestation's tine-attest/2 record must be signed by the DSSE envelope's key; the detail says record bound (0.41 records name signer_key) or record unbound (older records, still accepted). Keyless verification refuses (exit 6) a TUF-installed trusted root whose stored metadata has expired.

Push preview and manifest (0.36)

tine push REMOTE --dry-run (alias --outgoing) prints, as JSON, exactly what the push would send, and opens no connection: each ref as old -> new (the old side is the local remotes/<name>/... tracking ref from the last fetch, so the list is an upper bound if the remote has moved), the objects by kind with their stored bytes, and every run with the redaction ruleset it recorded (null for a run that recorded none). It honours --ref, --all and --remote-ref.

--manifest PATH writes the same document as canonical JSON (JCS) -- on a dry run, or beside a real push before it starts. --key-file (HMAC) or --ed25519-key-file signs it with a tine-sig/3 header block over opentine.push-manifest.v1: + JCS of {body, header}; --key-id names the key. A key flag without --manifest or --dry-run is refused.

git config tine.transcriptRemote NAME (a git remote name, URL or path) makes the tine git install-hook --push pre-push hook send refs/tine/bundle -- the runs named by Tine-Run: trailers -- to that remote instead of the code remote, keeping transcripts off the code host.

Run summaries: tine explain (0.42)

tine explain RUN                 # text summary; stored as an annotation in a v3 repo
tine explain RUN --json          # {"command": "explain", "run", "digest", "summary",
                                 #  "stored", "annotation", "narrative"}
tine explain RUN --no-store      # print only
tine explain RUN --narrative     # plus a narrative from a local model

A deterministic, offline summary built from the recorded run alone: what was asked, models, steps by kind, tools used, files written and read, hosts, the outcome, errors and failed tool calls, and tokens and cost when the run records them (absent otherwise, never 0). The same run always gives byte-identical output. summary.schema is opentine-explain/1; digest is the SHA-256 of the summary as sorted, compact JSON.

In a v3 repository (--repo PATH, or one in the current directory) the summary is stored on the run's annotation chain under metadata["opentine.explain"]; re-running it writes nothing new. A legacy .tine run is explained but not stored.

--narrative asks the model named in OPENTINE_EXPLAIN_MODEL (provider[:model], for example ollama:llama3.1) for a few sentences. Only a local provider is accepted; with none configured the command refuses with exit code 2 and sends nothing. The narrative is stored apart from the summary, under metadata["opentine.explain.narrative"], with generated_by: "model", a label saying it is model-generated and not evidence, the model spec and the digest of the summary it describes.

Line attribution (0.42)

tine git attribute COMMIT [RUN...] [--sign-key KEY] [--no-configure] [--json]
tine git attribution COMMIT [--verify] [--pubkey KEY.pub] [--json]
tine git attribution COMMIT --format agent-trace
tine stats --ai-share [--rev REV] [--json]

tine git attribute diffs COMMIT against its first parent and matches each added line to the recorded write that inserted it (the matching tine blame uses), over the runs given, or else the runs the commit already links through Tine-Run trailers or notes. The result is stored in the commit's refs/notes/tine note as one Tine-Attribution: {...} line (schema opentine-line-attribution/1): per file, ranges with start_line, end_line, the run, step, seq, tool and model that wrote them, and the content_hash (SHA-256) of the lines covered. Tine-Run lines for the runs are kept or added, so tine git log sees the same note. With --sign-key the record carries an Ed25519 tine-sig/3 block under the domain opentine.line-attribution.v1:.

The record names no commit, so it stays true when git copies the note to a rewritten commit. tine git attribute adds this to the repository's config (skip with --no-configure):

git config --add notes.rewriteRef refs/notes/tine

With it, git commit --amend and git rebase (including rebase -i) copy the note to the new commit. Set it yourself in clones where attribute has not run; git does not copy notes by default.

tine git attribution COMMIT --verify checks the signature (against --pubkey, or the embedded key as trust on first use) and that every range's hash still matches the lines the commit adds; it exits 1 when either fails. --format agent-trace prints the note as an Agent Trace record: vcs is {"type": "git", "revision": COMMIT}, one conversation per model with contributor ai, ranges with content_hash, and run and step ids in metadata["io.github.0xcircuitbreaker.opentine"].

tine stats --ai-share blames every text file at --rev (default HEAD) and counts the lines whose originating commit's attribution note credits them to a run: total_lines, ai_lines, ai_share, by_model, per-file counts and commits_with_attribution. Lines in commits without a note count as not attributed.

Fleet coordination: tasks, leases and steering (0.43)

CommandWhat it does
tine task add ID [--title T] --repo PATHCreates coord/tasks/ID (opentine-coord/1)
tine task claim ID --holder NAME [--ttl S] --repo PATHTakes the lease by one compare-and-swap ref move; prints the new fencing token
tine task heartbeat ID --holder NAME --fencing N [--ttl S]Extends the lease; refused (stale) once it expired or was re-claimed
tine task done ID --holder NAME --fencing NCloses the task; refused for a stale holder
tine task list --repo PATHEvery task, its status, holder, token and lease end
tine fleet resume fleet.toml --id ID [--trust]Runs the members of fleet ID that did not finish; the manifest digest must match the record
tine fleet logs ID [--member KEY]Prints each member's captured output
tine fleet send ID --member M --message TEXT --repo PATHQueues a steer message in coord/mailbox/ID/M

Every task and mailbox version is an annotation chained by previous_id, so the ref's history is the record's audit trail. Of any number of racing claimants exactly one compare-and-swap wins; an expired lease is re-claimed with a higher fencing token. With --json each task verb prints {"command": "task <verb>", "task": {...}} ("tasks": [...] for list); a refused operation exits 1 with {"error": {"code": "held|stale|done|exists|missing|contended", ...}}.

A sent message is delivered by the member's tine hook capture at its next turn end (the hook inherits OPENTINE_FLEET_ID and OPENTINE_FLEET_MEMBER from tine fleet up), once, and recorded as a human step with actor steer. The generic response carries "steer": [TEXT, ...]; Claude Code's Stop hook gets {"decision": "block", "reason": TEXT}, so the agent continues with the message. Capture must be enabled (hooks.capture).

tine serve (0.43) also takes POST /v1/tenants/T/ref-txn with {"updates": [{"name", "new", "expected_old"}, ...]} (at most 64): every move is checked exactly as PUT /refs/<name> checks it, then all are applied in one index transaction or none (409 {"updated": false}). The server lists "ref_txn": true in /v1/capabilities; tine push then moves the head and the run's annotation ref in one transaction, and keeps one PUT per ref against a server that does not list it.