omni-dev keeps a local, append-only log of every invocation and the HTTP
requests it issues, and ships an omni-dev log subcommand to search and
pretty-print it. It is the single, durable, queryable record of what was run
and what it talked to over the network — the thing RUST_LOG tracing (ephemeral,
stderr-only) is not.
One JSON object per line (NDJSON). A kind field discriminates the two record
types, so the log is a complete invocation history, not just an HTTP history:
-
kind: "invocation"— one per process run (and one per MCP tool call): the resolved subcommand path, full argv (with secret-bearing flag values redacted), exit code, duration, any top-level error, and a whitelistedOMNI_DEV_*env snapshot. -
kind: "http"— one per outbound request (recorded inside each client's retry loop, so retries and transport failures are captured too): service, method, URL (secret-bearing query/fragment parameter values redacted — see Redaction posture), status, elapsed, and any error. -
kind: "gh"— one perghCLI subprocess invocation. Every GitHub call funnels throughgh(the token never enters our process), so these are subprocess records, nothttpones. Each carries the semantic subcommand (api graphql,pr list,repo view, …) incommand, the scrubbed argv incommand_line, thesource, the exit code, and the duration. This is whatomni-dev log count --kind ghaggregates — see Counting GitHub API calls. -
kind: "worktree"— one peromni-dev git worktree <verb>invocation (thin logged wrappers overgit worktree add/remove/list/move/prune/repair, #1392). Taggedservice: "worktree", withcommandset to["git", "worktree", "<verb>"]and the recovery-relevant metadata in the free-formcontextmap — forremove: the worktree'spath,branch,commit(HEAD before removal),had_uncommitted, andused_force; forprune: a JSONprunedlist of{path, branch, commit}. See Recovering a removed worktree. -
kind: "drivemutation"— one peromni-dev drive rename/drive move/drive create/drive upload/drive edit/drive sheets *attempt, written from inside the mutation itself so it covers every current and future caller (CLI today, MCP later), not just the CLI. Includes a refused move (Blocked, ADR-0070) or create/upload/edit (Blockedby the folder write-permission gate, ADR-0071, issue #1574) — no mutating API call happens for those, but the refusal is itself the security-relevant event. Taggedservice: "drive", withcommandset to["drive", "<operation>"]and the outcome detail in the free-formcontextmap:file_id,file_name,status, and — only when present —added_principals/removed_principals(comma-separated,move-only) andcrosses_drive_boundary(move-only), plusresolved_folder_id/decided_by_folder_id/decided_by_depth(the folder the write-permission gate evaluated against, and which configured rule, if any, decided the verdict — absent for the ungatedrename/move), anddecided_by_file_idwhen a file rule decided it (issue #1612).decided_by_file_idis mutually exclusive withdecided_by_folder_id, deliberately: a file id never appears in the folder field, so an existing--query decided_by_folder_id:<id>cannot start matching a different kind of id. A file rule matches the target itself, so it walks no chain —decided_by_depthandresolved_folder_idare both absent alongside it. See docs/drive.md.Cell writes through the Sheets API (issue #1589, ADR-0073) use this same kind, with
operationofsheets-write/sheets-append/sheets-clear/sheets-create— socommandreads["drive", "sheets-write"]even though the CLI spells itdrive sheets write. They add five more omit-if-absent context keys:range(the A1 range as composed and sent), andupdated_range,updated_rows,updated_columns,updated_cells(what the API reported actually changing).updated_rangecan differ fromrange— the server resolves an open-ended range against the sheet's real extent — which is what makesomni-dev log --query kind:drivemutationable to answer "how much did that write touch", not just "a write happened".Structural edits (issue #1613, ADR-0075) use the same kind again, with
operationofsheets-add-sheet/sheets-rename-sheet/sheets-insert-rows/sheets-insert-columns— one record per verb, which is also one perspreadsheets.batchUpdaterequest, since each verb sends a batch of exactly one. They add four more omit-if-absent context keys:sheet_id(the stable numeric id the API addresses, which survives a later rename and so is the only durable answer to "which tab was this"),sheet_title(its title at the time),sheet_new_title(the title arename-sheetmoved it to — set by that verb alone, sincesheet_titlenecessarily holds the title the sheet had before, and without it a record could say which tab was renamed but not to what), anddimension_range(e.g."ROWS 5:7", 1-based inclusive like the CLI's--at).dimension_rangeis the structural analogue ofrange, for effects A1 notation cannot express, and is omitted for a span that spans nothing; a structural verb sets norangeand no cell counts.Destructive edits (issue #1623, ADR-0077) use the same kind again, with
operationofsheets-delete-sheet/sheets-delete-rows/sheets-delete-columns/sheets-delete-range— same one-record-per-verb-and-per-request shape as the structural edits above, and the same four structural context keys (sheet_id,sheet_title,dimension_rangefor the two dimension-delete verbs). They add one more omit-if-absent context key:grid_range(e.g."rows 2-4, columns 2-3", 1-based inclusive like the CLI's--start-row/--end-row/--start-column/--end-column) — thedeleteRangeanalogue ofdimension_range, which only ever spans one axis and so cannot record a rectangle.delete-sheetsets neitherdimension_rangenorgrid_range. Every--dry-run, additive or destructive, is unlogged —record_attemptonly ever runs for a real mutation attempt — and a destructive one stays structural-only besides: no cell content is ever read or logged for any structural verb, delete included.Formatting, data validation and protected ranges (issue #1643, ADR-0078) use the same kind again, still one record per verb:
update-borderscan set up to fourBordersides in a single request, which is the first case where one request carries more than one independently-toggleable effect, but it is still one request, so the one-record-per-verb-per-request equivalence ADR-0075 §7 established holds. Newoperationvalues:sheets-format-cells,sheets-update-borders,sheets-merge-cells,sheets-unmerge-cells,sheets-auto-resize-dimension,sheets-update-dimension-properties,sheets-duplicate-sheet,sheets-reorder-sheet,sheets-hide-sheet,sheets-show-sheet,sheets-set-data-validation,sheets-clear-data-validation,sheets-protect-range,sheets-update-protection,sheets-unprotect-range. Six more omit-if-absent context keys:fields_changed(a human-readable summary of whichCellFormat/border/dimension-property fields a verb set — the formatting analogue ofdimension_range, needed because a single request here can carry more than one named effect),discarded_cells(merge-cellsonly — the non-top-left, non-blank cells a merge discarded, as"A1: value"strings, so the one formatting request that destroys data has a record saying exactly what was lost),validation_type(the condition type aset-data-validationapplied, or"cleared"),protected_range_id(the stable numeric id of a protected range a protection verb acted on — server-assigned forprotect-range, otherwise the one resolved against), andprotection_editors_added/protection_editors_removed(comma-separated).Text writes through the Docs API (issue #1615, ADR-0076) use this same kind, with
operationofdocs-replace/docs-append, and add three more omit-if-absent context keys:occurrences_changed(what the server reported changing, which can differ from the client-side--dry-runestimate),inserted_chars, andrequired_revision_id(the revision lease presented, recorded so astale-revisionrefusal is as auditable as a success — an opaque, short-lived id, not a secret).The searched, replacement and appended text are never recorded. They are user prose, and often the most sensitive thing in the invocation — sharper than the Sheets case, where an A1 range is metadata rather than content. Only counts and the opaque revision id go in.
-
kind: "audit"— written to the separateaudit.jsonlsink, never to this file — see Audit log. Every otherkindabove stays exactly as described regardless of the audit log's existence: a leased Drive write (issue #1664, ADR-0080) still produces its normaldrivemutationrecord here, correlated to itsauditrecord by the sharedinvocation_id.
Every HTTP, gh, worktree, and drivemutation record shares an
invocation_id with the invocation that issued it, so you can pull a run and
all of its requests with a single --id.
Coverage caveat: omni-dev's own in-process HTTP clients and its Rust gh
calls (daemon and one-shot CLI) are recorded. The one gap left is the VS Code
companion extension's gh pr list, which runs in a separate Node process and
does not write to this log.
Resolved in this order:
OMNI_DEV_LOG_FILEif set.- Otherwise
dirs::state_dir()joined withomni-dev/log.jsonl— on Linux,~/.local/state/omni-dev/log.jsonl. - On platforms without a state dir (macOS), it falls back to the data dir,
matching the daemon's convention:
~/Library/Application Support/omni-dev/log.jsonl.
The directory is created 0700 and the file 0600, the same posture as other
omni-dev runtime state. The log lives entirely on your machine.
| Variable | Effect |
|---|---|
OMNI_DEV_LOG_FILE |
Override the log path. |
OMNI_DEV_LOG_DISABLE=1 |
Disable logging entirely. |
OMNI_DEV_LOG_BODIES=1 |
Opt in to recording request/response bodies (off by default; payloads are large and may contain customer content). |
OMNI_DEV_LOG_HEADERS=1 |
Opt in to recording (redacted) request/response headers. |
OMNI_DEV_LOG_MAX_SIZE |
Enable automatic size-capped rotation on write, e.g. 10mb (unix only; see Bounding growth). |
OMNI_DEV_LOG_KEEP_FILES |
Number of rotated files to keep when rotation is enabled (default 3). |
OMNI_DEV_AUDIT_LOG_FILE |
Override the audit log path — see Audit log; none of the other variables above affect it. |
Logging is best effort: a write failure is swallowed (logged only at
tracing::debug) and can never change the command's exit code. The audit log
is the deliberate exception — see Audit log.
omni-dev log [OPTIONS] # search (default)
omni-dev log prune [OPTIONS] # trim the log — see Bounding growth
With no subcommand, omni-dev log searches and prints the log using the filters
below. The prune subcommand trims it; see Bounding growth.
| Flag | Matches |
|---|---|
--since <DUR_OR_TS> |
Lower time bound: a relative window (30m, 2h, 1d, 1w, 45s), a date (2026-07-01), or an RFC3339 timestamp. |
--until <DUR_OR_TS> |
Upper time bound: same forms as --since (a relative value means that long ago). Pair with --since for a bounded window. |
--method <METHOD> |
HTTP method (case-insensitive). |
--status <STATUS> |
Exact (200), class (5xx), or comma list (4xx,5xx). |
--service <NAME> |
jira, confluence, datadog, browser-bridge, snowflake, transcript, anthropic, bedrock, openai, ollama, claude-cli. |
--command <PATH> |
Resolved command-path prefix on whole segments, e.g. "jira read". |
--url <SUBSTR> |
Substring of the request URL. |
--grep <REGEX> |
Regular expression against the raw JSON line. |
--fuzzy <TOKEN> |
Substring of the raw line; repeatable, AND-ed. |
--query <EXPR> |
Query expression (see below); repeatable, AND-ed. |
--id <ID> |
This record id or invocation_id — pulls a run and its requests. |
| Flag | Effect |
|---|---|
-o, --output <oneline|json|full> |
oneline (default), json (NDJSON, byte-identical to the file — composes with jq), or full (pretty block). |
-n, --limit <N> |
Show at most the N most recent matching records. |
-f, --follow |
Tail the log, printing new matching records as they arrive. |
--audit |
Read audit.jsonl instead of log.jsonl — see Audit log. Every filter, the --query mini-language, and all three output formats apply unchanged; only the file being read differs. |
- Structured terms:
field:value—kind,source,service,method,status(supports5xx),command,url,id,invocation_id(aliasinv),mcp_tool(aliastool),via_daemon,error, plus the fields that were previously reachable only through--grep:exit_code(aliasexit),duration_ms(aliasesduration,dur),elapsed_ms(aliaselapsed),hostname(aliashost),system_user(aliasuser),cwd, andauth_principal(aliasprincipal). Field matching is shared with the flags, so--status 5xxandstatus:5xxbehave identically. statusis kind-aware: for every kind butdrivemutationit matches the HTTPstatus_code(class or comparator syntax, below). Adrivemutationrecord has nostatus_code— its domain status (blocked,written,stale-revision, ...) lives in thecontextmap instead, sostatus:there matchescontext["status"]by exact case-insensitive equality, e.g.kind:drivemutation status:blocked.- Numeric comparisons: numeric fields (
exit_code,duration_ms,elapsed_ms, andstatus) accept a leading comparator —>,>=,<,<=, or bare=/Nfor equality — so slow requests and failed runs are expressible directly (elapsed:>1000,exit_code:>0).statusstill also accepts its class syntax (5xx). Comparator/class syntax applies only to the HTTPstatus_codepath, not thedrivemutationcase above. - Text fields (
url,hostname,system_user,cwd,auth_principal) match a case-insensitive substring. - Context fields: any other field name falls back to the record's free-form
contextmap (case-insensitive substring), so worktree recovery fields query directly:branch:issue-1392,path:demo-wt. - Bare tokens are fuzzy substring matches against the raw JSON line.
- Operators:
AND(also implicit between adjacent terms),OR,NOT(or a leading-), and parentheses. Use"quotes"for a value containing spaces.
omni-dev log --query 'kind:http AND (status:5xx OR method:POST)'
omni-dev log --query 'service:jira -status:2xx' # jira requests that did not 2xx
omni-dev log --query 'elapsed:>1000' # requests slower than 1s
omni-dev log --query 'kind:invocation exit_code:>0' # failed runs# The last 20 things you ran.
omni-dev log -n 20
# Server errors in the last two hours.
omni-dev log --since 2h --status 5xx
# Slow datadog requests (native — no jq needed).
omni-dev log --service datadog --query 'elapsed:>1000'
# A bounded historical window.
omni-dev log --since 2026-07-01 --until 2026-07-02
# A run and every request it made.
omni-dev log --id 0001718000000-0a1b2c3d4e5f6071
# Compose with jq for anything not directly expressible.
omni-dev log -o json --service datadog | jq 'select(.status_code == 429)'
# Follow live.
omni-dev log -f --service browser-bridgeomni-dev git worktree remove records the worktree's branch, HEAD commit, and
dirtiness before the removal, so an accidental remove is recoverable minutes
later from the log alone:
# What worktree did I just remove?
omni-dev log --command 'git worktree remove' --since 1d -o full
# Or by any recovery field directly.
omni-dev log --service worktree --since 30m
omni-dev log --query 'branch:issue-1392'The full record's context carries path, branch, commit,
had_uncommitted, and used_force — enough to re-create the branch
(git branch <branch> <commit>) and re-attach a worktree
(omni-dev git worktree add <path> <branch>). prune records carry the same
triple per pruned entry in a JSON pruned list.
omni-dev log count aggregates the log by record kind and source over an
optional window — a quick tally without paging through matches:
omni-dev log count [--since <DUR_OR_TS>] [--until <DUR_OR_TS>] [--source <SOURCE>] [--kind <KIND>] [--json]
| Flag | Effect |
|---|---|
--since <DUR_OR_TS> |
Lower time bound — same forms as omni-dev log --since (1h, 2026-07-01, RFC3339). |
--until <DUR_OR_TS> |
Upper time bound (a relative value means that long ago). |
--source <SOURCE> |
Restrict to cli, daemon, or mcp. |
--kind <KIND> |
Restrict to invocation, http, gh, worktree, or drivemutation. gh unlocks the GitHub breakdown (below). |
--json |
Emit JSON (string map keys, composes with jq) instead of the table. |
With no --kind it reports the total and the split by kind and source:
# What has this machine done in the last day?
omni-dev log count --since 1dBecause every GitHub call is recorded as a kind: "gh" line (see What gets
recorded), --kind gh turns the log into a ground-truth
count of the GitHub API calls omni-dev itself makes — the local counterpart
to gh api rate_limit, which is GitHub's server-side view of everything the
token spent. It renders a richer breakdown by category — api (gh api …),
subcommand (gh pr list, gh repo view, …), and local (gh --version,
excluded from the API total) — as well as by subcommand and by source:
# All GitHub API calls in the last hour.
omni-dev log count --kind gh --since 1h
# Just what the daemon's pollers spent today, as JSON.
omni-dev log count --kind gh --since 1d --source daemon --json
# The raw records (composes with jq / anything log can express).
omni-dev log --query 'kind:gh'The counts share the request log's storage, so they honor the same window and
are subject to prune/rotation. The daemon also logs a summary of these counters
to its own log (daemon.log / the journal) ~5 seconds after startup, every 10
minutes, and once on shutdown — a periodic, restart-delimited footprint without
anyone running a command; daemon status surfaces the current count too.
Not yet counted: the VS Code companion extension's gh pr list runs in a
separate process and does not write to the log (a planned follow-up).
The log is default-on for every invocation and every outbound request, so on an active machine it grows steadily. Two opt-in bounds keep it in check; both are off by default, so nothing changes unless you ask for it.
Trims the log in place, by age and/or by size:
omni-dev log prune [--older-than <DUR>] [--max-size <SIZE>] [--dry-run]
| Flag | Effect |
|---|---|
--older-than <DUR> |
Remove records older than a relative window (7d, 24h, 2w, 45m). |
--max-size <SIZE> |
After age pruning, drop the oldest records until the file is at most <SIZE> (10mb, 512kb, or a bare byte count). |
--dry-run |
Report what would be removed without modifying the file. |
At least one of --older-than / --max-size is required. Sizes are binary
(kb/mb/gb = 1024-based). Records with a missing or unparseable timestamp
are kept (age pruning only removes records it can positively date as old), and
--max-size always keeps at least the single most recent record.
# Keep the last 30 days; preview first.
omni-dev log prune --older-than 30d --dry-run
omni-dev log prune --older-than 30d
# Cap the file at 20 MB, dropping the oldest records to fit.
omni-dev log prune --max-size 20mbPruning rewrites the file atomically (a same-directory temp file is renamed over
the original, preserving the 0600 mode), so a concurrent reader never sees a
half-written file. It is not locked against concurrent writers, though: a
record appended during the rewrite may be lost, and — because prune is itself
a logged invocation — pruning the active log appends one new record of its own.
For exact accounting, prune with OMNI_DEV_LOG_DISABLE=1 or when the log is idle.
Set OMNI_DEV_LOG_MAX_SIZE (e.g. 10mb) to rotate on write: before an append
that would push the file past the cap, log.jsonl is renamed to log.jsonl.1
(shifting any existing log.jsonl.1 → .2, and so on) and a fresh log.jsonl
is started. OMNI_DEV_LOG_KEEP_FILES (default 3) bounds how many rotated files
are retained; the oldest beyond that is deleted. Total on-disk use is therefore
roughly (OMNI_DEV_LOG_KEEP_FILES + 1) × OMNI_DEV_LOG_MAX_SIZE.
Rotation is unix-only and best effort: when it is enabled, writers
serialize on a stable log.jsonl.lock file (created 0600) for the
check-rotate-append sequence, and a rotation failure falls back to appending
without rotating rather than dropping the record. The env vars are read per
write, so they must be present in the environment of whatever writes the log
(your shell for CLI runs, or the daemon's environment for daemon-served
requests). A set-but-invalid OMNI_DEV_LOG_MAX_SIZE is ignored (logged at
tracing::debug) and leaves rotation off. The omni-dev log reader already
tolerates truncation and rotation, so -f/--follow keeps working across a
rotation (it restarts from the top of the fresh file).
audit.jsonl is a separate file, a sibling of log.jsonl under the same
runtime directory, reusing the same LogRecord schema and the same
omni-dev log reader — pass --audit to read it instead of log.jsonl; see
Filters/Output above, all of which apply unchanged.
It exists to serve the opposite guarantee from everything above: where
log.jsonl is best-effort, prunable, and can be disabled outright,
audit.jsonl is fail-closed — an operation whose audit record cannot be
written does not happen — and exempt from every growth bound this page just
described:
log.jsonl |
audit.jsonl |
|
|---|---|---|
| On a write failure | Swallowed; the command's exit code is unaffected | Propagated; the caller aborts the operation it was about to audit |
OMNI_DEV_LOG_DISABLE=1 |
Suppresses all writes | No effect |
omni-dev log prune |
Trims by age/size | Refuses --audit outright (log prune does not support --audit), and refuses any resolved log path that names the audit file — e.g. OMNI_DEV_LOG_FILE pointed at audit.jsonl directly, via a .. segment, or via a symlink |
OMNI_DEV_LOG_MAX_SIZE rotation |
Applies | Never applies, regardless of the setting or how OMNI_DEV_LOG_FILE is spelled |
| Path override | OMNI_DEV_LOG_FILE |
OMNI_DEV_AUDIT_LOG_FILE |
| Location when unset | <state dir>/omni-dev/log.jsonl |
<state dir>/omni-dev/audit.jsonl |
--audit is a flag on the bare search form (omni-dev log --audit ...) and,
separately, on prune (omni-dev log prune --audit, always refused, above);
placed before a subcommand name instead — omni-dev log --audit prune ... —
it is refused rather than silently ignored, since a subcommand never
consults it. record_audit also refuses to write if OMNI_DEV_LOG_FILE and
OMNI_DEV_AUDIT_LOG_FILE are configured to resolve to the same file —
compared by file identity (the same-file crate — inode on unix, a file
handle on Windows — falling back to a normalized path when one side doesn't
exist yet), not merely by identical spelling, so a .. segment,
a relative-vs-absolute spelling, or a symlink can't slip past the check: the
two sinks are siblings by default but not otherwise mutually exclusive by
construction, so misconfiguration is caught at write time instead of
silently blending the fail-closed sink into the best-effort one. That guard
only stops writes into log.jsonl; a colliding OMNI_DEV_LOG_FILE can
still cause a best-effort record to land in audit.jsonl (it just can no
longer be pruned or rotated away, per the table above) — the next
record_audit call still fails loudly, surfacing the misconfiguration.
Every record carries kind: "audit" and, in context, an integration key
("drive" today, so a later integration's audit trail is additive rather
than a rename), a file_id, and a verdict — alongside the usual
invocation/HTTP fields. RecordKind::Audit is the schema landed by issue
#1664
(ADR-0080 §11); what populates it is the Drive
leased-write lifecycle, landing across that same issue's phases.
drive lease acquire writes one record per attempt, command: ["drive", "lease-acquire"], regardless of outcome — verdict is acquired,
acquired-headless-waiver (ADR-0080 §8/§13, issue #1677: proceeded under the
headless opt-out, no human ever prompted), already-leased,
refused-native-document, denied, unavailable, or failed, matching
AcquireResult's own kebab-case status — plus, on already-leased/failed,
the -backup-orphaned suffix (issue #1690): this attempt took a real backup
before discovering it isn't referenced by any ledger row, and reclaiming
that backup itself then failed, so it is now truly orphaned (drive lease prune cannot see it either). An acquired record additionally carries
lease_id (the token), version_after/modified_time_after (the Drive
state actually recorded into the ledger — re-read after the backup, not the
pre-authentication snapshot, for the same TOCTOU reason the ledger itself
does), backup_location (a local path for a byte backup, or the backup
copy's own file id for a native-document backup),
backup_sha256/backup_size (byte backups only), and auth_policy
(device-owner or biometrics-only). An already-leased/failed record
also carries backup_location whenever this attempt took a backup, whether
or not reclaiming it succeeded — the -backup-orphaned suffix is what
distinguishes the two. Unlike a leased write's own record (below), this
one is best-effort rather than write-ahead/fail-closed: acquiring mutates no
Drive content — by the time the record is written the consent, the backup
and the ledger row have already durably happened, so a
logging failure is warned and does not turn a successful acquisition into a
reported failure.
A leased write produces records in both files, correlated by
invocation_id: its ordinary drivemutation record here in log.jsonl
(ADR-0070/0071/0073/0075/0076/0077/0078), and, in audit.jsonl, one or
two audit records from crate::drive::lease::check — shared by every
leased-write engine (drive edit, the five Sheets engines and drive docs write). Their command is ["drive", <operation>] with the verb's
operation name — ["drive", "edit"], ["drive", "sheets-delete-sheet"],
["drive", "docs-replace"] — byte-for-byte the command of the same
write's drivemutation record, so --query 'command:sheets-delete-sheet'
matches both files and the audit record names the verb that ran, not
merely which engine ran it:
- A refusal that never reaches the mutating call writes one best-effort
record —
verdict: refused-no-lease,refused-lease-expired,refused-lease-wrong-fileorrefused-lease-stale, mirroring the engine's own reported result, orfailedwith theerrorwhen the ledger lock could not be taken. There is no mutation to pair a write-ahead record around. - A presented lease that checks out writes a write-ahead
pendingintent record carryinglease_id,version_beforeandmodified_time_before,fsynced before the mutating API call this lease authorises, and fail-closed: if this record cannot be written the write itself is refused rather than proceeding unaudited (the one place in the whole ADR this contract applies, per §11). After the mutating call returns, a best-effort outcome record follows with the samelease_id:verdict: allowedwithversion_after/modified_time_after(fromfiles.update's own response fordrive edit, from the post-writefiles.getthe ledger refresh already pays for on Sheets/Docs), orverdict: failedwith the APIerror— including a Docs412onwriteControl.requiredRevisionId, which the CLI reports asstale-revision: the lease was not stale, the mutation simply did not happen, and the error says why.
An intent record with no matching outcome is itself the signal that something was interrupted mid-write — the whole point of writing the former durably before the latter can even be attempted.
drive lease restore <TOKEN> writes its own top-level, best-effort
record, command: ["drive", "lease-restore"], in addition to the
generic write-ahead/outcome pair its internal fresh-lease acquisition and
restore write already produce (both keyed on the fresh token). This one
names both tokens (ADR-0080 §10/§11): lease_id is the fresh token
once minted (or the existing one, for already-leased), and
restored_from_lease_id is always the backup lease's own token — the
<TOKEN> argument — so --query 'restored_from_lease_id:<token>' finds
every restore attempt made from one backup regardless of outcome. verdict
matches drive lease restore's own reported status: restored,
restored-sheet, restored-headless-waiver,
restored-sheet-headless-waiver, sheet-already-restored,
no-such-backup-token, no-typed-restore-path,
backup-too-large-for-simple-upload, refused-no-visible-parents,
blocked, already-leased, refused-native-document, denied,
unavailable, failed, or fresh-lease-but-write-failed.
No secret material is ever written, under any code path:
- Auth headers/tokens are redacted centrally before writing; only a
non-secret
auth_principalidentity is ever kept. Redaction matches both a fixed list of known header names and any name containingauth,token,key,secret,cookie,password,session,signature, orcredential(case-insensitive). - URL query and fragment parameters whose keys look secret-bearing have their
values replaced with
REDACTEDbefore writing: keys suffixedtoken,secret,password,passwd,signature, orapi_key/apikey; the exact keyssig,sas,jwt, andauth; and theX-Amz-*/X-Goog-*signed-URL families. Host, path, and parameter keys are preserved, so--urlsubstring filtering keeps working. This matters mostly for the browser bridge, which logs arbitrary operator-supplied target URLs (presigned URLs,?access_token=…). - Request/response bodies are opt-in via
OMNI_DEV_LOG_BODIES=1. - The
OMNI_DEV_*env snapshot redacts any name containingTOKEN,SECRET,KEY,PASSWORD, orPASSWD. - Argv in the invocation record is scrubbed before writing, in both
--flag valueand--flag=valueforms:--headervalues naming a sensitive header are redacted keeping the name (Authorization: REDACTED), inline--bodyvalues are redacted (@filereferences are kept), and any flag whose name has atoken/secret/password/passwd/keysegment has its value redacted (flags ending in-file/-pathcarry paths and are exempt). Every argv element is then run through the same URL query/fragment redaction as--urlabove, so a secret carried in a URL argument (e.g.--url /path?access_token=…or a presigned target) is redacted even though--urlis not a secret-bearing flag name; benign argv passes through byte-identical.
The guarantees above keep secret material out of the log; they do not make prompt content secret. AI prompts carry whatever you asked about — repo diffs, commit messages, JIRA/Confluence data — and that content surfaces on three paths:
OMNI_DEV_LOG_BODIES=1records AI request/response bodies, which are the prompts.RUST_LOG=debugstderr tracing emits the full system/user prompt and full response from every AI backend. Tracing is ephemeral and stderr-only, but if you redirect it to a file or paste it into a bug report, prompt bodies go with it.- On a
claude-clisubprocess failure, the returned error embeds the child's stdout/stderr verbatim, which can carry prompt-derived content into whatever captures the error (terminal, CI logs, this log'serrorfield).
No API keys or tokens appear on any of these paths — this is a data-sensitivity note, not a credential leak.
Requests executed inside the daemon (the browser bridge and the Snowflake
session pool) set via_daemon: true. Although the daemon is a separate process
from the CLI that asked it to act, such requests are still stamped with the
originating CLI invocation's invocation_id: the thin client threads its id
across to the daemon — on the control socket for Snowflake (an
origin_invocation_id envelope field) and on the X-Omni-Bridge-Origin header
for the bridge — and the daemon scopes it around the request it serves. So
omni-dev log --id <cli-invocation> surfaces the via_daemon requests that run
triggered, correlating both halves of one logical operation
(#1198).
The threaded id is non-secret (it is not a token), and the record's source
stays daemon, so a daemon-served request remains distinguishable by
via_daemon / source even while its invocation_id points at the caller. The
same mechanism correlates a standalone bridge serve (there source stays
cli and via_daemon is unset).
Records are read and written through a single forward-compatible struct: every
field is #[serde(default)] and every optional field is skip_serializing_if.
A newer reader never chokes on an older line, and an older reader never chokes on
a newer one — the same forward-rolling contract the daemon wire types use. The
record id is time-sortable, so sorting lines by id ≈ sorting by time.