zirvDocs

DocsSupervision (zirv ctx)

Configuration

README.md, lines 4749–5629View source

Layered, lowest priority first: ~/.zirv/ctx.toml, then <repo>/.zirv/ctx.toml, then ZIRV_CTX_* environment variables, then flags. A .zirv/ directory that is the operator's own (running zirv/zirv chat from the home directory itself) is never also read as the repository layer for ctx.toml or for the system-prompt.md prompt layer.

toml
# .zirv/ctx.toml
agent = "claude"

[score]
window = 10
min_turns = 10
token_floor = 100000
token_ceiling = 160000
marker = "[zirv]"
advise_at = 40
compact_at = 60
restart_at = 80

[wrap]
debounce_ms = 3000
inject_timeout_ms = 20000

[supervise]
max_restarts = 2
interval_secs = 900
max_cycle_secs = 3600
max_failures = 5

[handoff]
model = "haiku"
tail_items = 5
timeout_secs = 30   # the distiller is given this long before the structural
                    # fallback is used instead

[mail]
enabled = true
max_message_bytes = 4096      # per-message cap, applied by `zirv ctx send`
max_delivered_bytes = 4096    # cap on a whole batch folded into one launch prompt
keep = 50                     # unread messages kept per repo before the oldest are pruned
mid_turn = false              # operator-only: deliver session-addressed mail from a Claude PostToolUse hook

[supervisor]                  # operator-only (#835); off by default, see "Supervisor"
enabled = false               # ZIRV_CTX_SUPERVISOR_ENABLED; off means no spawns, no state files, unchanged hook output
harness = "codex"             # claude or codex; ZIRV_CTX_SUPERVISOR_HARNESS
model = ""                    # explicit consult model (e.g. claude-fable-5-1 or gpt-6-astra), REQUIRED while enabled (config error otherwise); ZIRV_CTX_SUPERVISOR_MODEL
max_calls = 3                 # deprecated and ignored (consults are uncapped); still parsed so old configs load; ZIRV_CTX_SUPERVISOR_MAX_CALLS
max_advice_bytes = 2048       # cap on a ruling's reason; ZIRV_CTX_SUPERVISOR_MAX_ADVICE_BYTES

[models]
discovery = true              # read Codex's cache and model ids observed in Claude transcripts
price_fetch = true            # allow public-price fetches (explicit refresh and the automatic background refresh)
pin = { "anthropic.opus" = "claude-opus-5-5" } # optional family pins
avoid = []                    # model ids never resolved from a tier or rung; an explicit pin wins
auto_avoid = false            # also avoid models significantly worse than a same-tier peer (needs 20+ samples)

[price]
stale_after_days = 90
# table_path = "~/.zirv/prices.toml" # per-model operator overrides

[memory]
enabled = true
harvest = false                # opt-in; see "Memory bank" below
max_entries = 50               # entries kept per repo before the oldest (by Written) are pruned
max_entry_bytes = 512          # per-entry cap at injection; banks keep the full body, a longer one ends "[truncated: zirv ctx recall --key <key>]"
max_injected_bytes = 2048      # superseded by core_max_bytes; kept only so an old config does not error
shared_enabled = true          # whether the repo-owned shared bank (<repo>/.zirv/memory/) is read at all
core_max_bytes = 2048          # cap on the merged private+shared core layer folded into every session
retrieval_max_bytes = 2048     # cap on `zirv memory recall`; reserved for future context-aware session retrieval
retrieval_max_entries = 6      # max number of recalled entries, independent of bytes

[chrome]
banner = true   # the one-time launch banner
bar = true      # the reserved one-row status bar
events = true   # the `zirv ▸` announcement channel on stderr

[proxy]
enabled = false              # ZIRV_CTX_PROXY_ENABLED
decider = "typesafe"         # typesafe | helper | deterministic; ZIRV_CTX_PROXY_DECIDER
min_confidence = 0.5         # ZIRV_CTX_PROXY_MIN_CONFIDENCE
min_margin = 0.2             # ZIRV_CTX_PROXY_MIN_MARGIN
request_max_bytes = 16384    # ZIRV_CTX_PROXY_REQUEST_MAX_BYTES
validation_gate = false      # ZIRV_CTX_PROXY_VALIDATION_GATE

[proxy.typesafe]
base_url = "https://api.typesafe.ai/v1"   # ZIRV_CTX_PROXY_TYPESAFE_BASE_URL
credential_env = "TYPESAFE_API_KEY"       # ZIRV_CTX_PROXY_TYPESAFE_CREDENTIAL_ENV
model = "jev-1.13.0"                      # ZIRV_CTX_PROXY_TYPESAFE_MODEL -- pinned, not jev-latest
timeout_secs = 10                         # ZIRV_CTX_PROXY_TYPESAFE_TIMEOUT_SECS

# operator-only: which advisory sites besides the harness proxy may consult
# the shared Jev client; each is also gated on the `[proxy.typesafe]`
# credential actually being set (see "Harness proxy" above)
[jev]
memory = false      # deprecated and ignored (the memory rerank and harvest gate were removed); still parsed so old configs load, never counts as an enabled gate; ZIRV_CTX_JEV_MEMORY
supervisor = false  # judge pre-filter, crash triage, handoff quality (every stored handoff: wrap/exec restarts, handover swaps, pane rollovers, the harvest note, `zirv ctx handoff`; not the `--dry-run` preview); ZIRV_CTX_JEV_SUPERVISOR
dispatch = false    # model tier for an omitted Agent model, from brief metadata only; ZIRV_CTX_JEV_DISPATCH
review = false      # narrows review triage findings/effort; ZIRV_CTX_JEV_REVIEW
gates = false       # narrows workflow gate reclassification; ZIRV_CTX_JEV_GATES
context = false     # selects optional skill/report descriptions; ZIRV_CTX_JEV_CONTEXT
intake_savings = false # optional planner (clarification advice runs whenever decider=typesafe); ZIRV_CTX_JEV_INTAKE_SAVINGS
review_reuse = false # reuses an eligible converged review; ZIRV_CTX_JEV_REVIEW_REUSE
harvest_screen = false # may skip an optional memory-harvest generation call; ZIRV_CTX_JEV_HARVEST_SCREEN
admin_dispatch = false # closed-set read-only status/inbox answered without a model turn; ZIRV_CTX_JEV_ADMIN_DISPATCH
approve = false     # safety-hook risk check: sends local facts only (program/subcommand class, write/delete/network/privilege flags, path-scope class, pipe/redirect/substitution/secret-placeholder counts), may only escalate a deterministic allow to ask; makes no call at all for a read-only, worktree/scratchpad-confined command (grep/cat/head/git status/..., pipes between them); ZIRV_CTX_JEV_APPROVE (#781)
approve_allow = false # opt-in auto-approve, effective only when `approve` is also true: may lower a SIMPLE unmatched-default ask to allow (single segment, no pipe/redirect/substitution/env-prefix/code-bearing argument, program not a shell/eval/wrapper/refused-destructive program, not destructive/network/privilege) on a high-confidence/margin answer, with every check ALSO re-run on each token suffix to defeat launcher prefixes (nohup, timeout N, nice, ...); never a hard deny, and never a matched deny/ask rule (rm -rf, force-push, credential paths, ...) -- see "Command safety policy" below for the full structural rule; ZIRV_CTX_JEV_APPROVE_ALLOW (#781)
classify = false    # intent refinement for `zirv workflow start`/`classify` (classify also adds domain tags); ZIRV_CTX_JEV_CLASSIFY (#782)
handoff_select = false # keep/drop scoring of handoff candidate items; ZIRV_CTX_JEV_HANDOFF_SELECT (#783)
compaction_select = false # appends a short Jev-chosen keep list ("Keep in particular: ...") to a compaction's own focus text, from candidates (edited files, an unresolved failing test, first-prompt constraints, the latest plan) extracted deterministically from the transcript; ZIRV_CTX_JEV_COMPACTION_SELECT (#798)
inject_screen = false # warns (never strips) mail/worker-result text Jev flags as likely injected; ZIRV_CTX_JEV_INJECT_SCREEN (#784)
inject = false      # may only DEFER automatic compact/restart/mail/Stop-rot injections, within hard caps (operator mail and restart at the ceiling never wait); ZIRV_CTX_JEV_INJECT (#785)
stop_verify = false # facts-only check that may block a Stop once when edits are unverified and the closing message claims completion; ZIRV_CTX_JEV_STOP_VERIFY (#786)
missing_tests = false # when the deterministic `[missing_tests_gate]` is about to block, asks one metadata-only question from local numeric facts and skips that one block on a decisive "not owed" answer; ZIRV_CTX_JEV_MISSING_TESTS
retry = false       # after 3 identical tool failures in a row, asks Jev ONE retry/stop_and_ask/change_approach question per streak (numeric facts only: failure count, tool class, locally classified error class; never error text); a decisive stop_and_ask or change_approach (confidence >= 0.9, constant floor) adds one advisory line via the `PostToolUseFailure` hook (`zirv ctx hook tool-failure`, registered by `zirv setup apply` only while this key is on; re-run it after enabling); retry, a split answer or any error adds nothing. NOT yet probed for this exact question -- probe before enabling; ZIRV_CTX_JEV_RETRY (#836)
launch_effort = false # may steer a headless launch's first-turn CLAUDE_CODE_EFFORT_LEVEL pick, from local numeric facts only; see `[headless.effort]` below; ZIRV_CTX_JEV_LAUNCH_EFFORT
cache_ttl_secs = 604800  # 0 disables the cache; ZIRV_CTX_JEV_CACHE_TTL_SECS

Each gate defaults to false: Jev is operator-only (no repo config, only ~/.zirv/ctx.toml, ZIRV_CTX_JEV_*, or CLI flags). Endpoint credentials come from [proxy.typesafe] (shared with the harness proxy); zirv ctx jev status [--json] reports whether Jev is active and why not, distinguishing "no gate enabled" from "gate enabled but credential missing".

Measuring floor determinism. zirv ctx jev probe --site <SITE> --case <case.json> --reps <K> [--repo <dir>] (stdout is always JSON) asks one Jev site's real production question(s) for a fixture input K times (1..=20) with the cache disabled, applies that site's production floor (honouring [jev.floors.<site>]/ZIRV_CTX_JEV_FLOOR_<SITE>_MIN_CONFIDENCE|_MIN_MARGIN for the eight sites that have one -- the rest carry a fixed compiled constant, exactly as production does) and production answer-to-action rule, and prints what production would have DONE on each rep -- SITE is one of all twenty-two the probe contract names: context-report, context-skill, harvest-screen, handoff-thin, handoff-select, compaction-select, dispatch, launch-effort, classify-domain, inject, crash, judge, approve-escalate, approve-lower, intake-plan, inject-screen, missing-tests, stop-verify, review-disposition, review-dedup, artifact-substance, gate-reclass. case.json is {"id": "<case id>", "state": <the exact JSON state object production sends>, "n": <candidate count, required only for a per-candidate site>}; state is sent verbatim (still subject to jev::safe_metadata_request -- except artifact-substance /gate-reclass, whose production state is text-bearing and so never clears that boundary either; every rep for these two reports its fallback action with an "unsafe Jev metadata projection" error, mirroring production exactly). An unknown site, --reps outside 1..=20, a missing candidate count, a missing Jev credential, or an invalid ZIRV_CTX_JEV_PROBE_MIN_CONFIDENCE/ZIRV_CTX_JEV_PROBE_MIN_MARGIN (below) all exit 2. Output is one JSON object: {"site", "floor_site", "label", "floor": {"min_confidence", "min_margin"}, "item_floors": {"<item id>": {"min_confidence", "min_margin"}} (present only when a site's items do not all share one floor -- today only gate-reclass, whose work_domain question has its own default floor), "reps": [{"actions": {"<item id>": "<action>"}, "error": "<string or null>"}], "calls", "errors"} -- a rep whose call failed carries every item's fallback action (what production does on failure) plus its error text. Measurement only: it calls the exact same jev::advise_detailed entry point production calls (so jev-decisions.jsonl /jev-effects.jsonl and the spend ledger see real calls, spent for real), and has no other side effect. Two env vars are read ONLY by this probe, never by production: ZIRV_CTX_JEV_PROBE_MIN_CONFIDENCE/ ZIRV_CTX_JEV_PROBE_MIN_MARGIN, each a float in [0, 1], which REPLACE the resolved floor's matching field for whichever site is being probed (any [jev.floors] overlay is applied first); the output's floor object always reports the post-override effective values. An autoresearch campaign uses this to measure how deterministic each Jev feature's acted decision is under candidate floors.

Session relay. Every Jev call now goes through a process-wide keep-alive ureq::Agent instead of opening a fresh connection each time, which already helps any process making more than one call. Most [jev]-gated sites, though, run inside a short-lived, one-per-tool-call zirv ctx hook/zirv ctx safety check process, so there is nothing for that agent to keep alive across calls on its own. When at least one [jev] gate is on and a credential is present, zirv ctx exec/zirv ctx wrap's own long-lived supervisor hosts a small per-session relay for the duration of the session: an owner-only duplex endpoint (the same NDJSON transport zirv ctx api uses, <state>/s/jev-<hash-of-session-id> — a sibling of that command's own <state>/s/api.sock) that a hook process dials, over its own ZIRV_CTX_ SESSION, to reuse the supervisor's warm connection instead of paying a fresh TCP+TLS handshake. The client never sends its credential over that socket — the relay forwards with its own [proxy.typesafe] credential — and falls straight through to a normal direct call only when there is no relay to dial or the request cannot be handed to it. Once the relay has the request, the client waits up to timeout_secs plus 3 seconds for its answer and never re-sends it directly: a relay timeout, error or lost answer is recorded as a fallback and the deterministic path runs, so one decision is never billed twice. Caching, decision recording and error mapping are identical either way. A relay is always an optimisation: it is never required, and its absence never changes an answer, only how fast it arrives.

Jev requests now accept only a bounded numeric metadata envelope with static questions. The shared client rejects text, paths, diffs, secrets, dynamic question content, and legacy text-bearing advisory states before checking its cache or opening a connection (privacy fix #746). The new intake-savings path sends coarse categories and counts for clarification and optional planner advice; intent, risk, complexity, workflow, and seat choice remain deterministic. The supervisor and review paths likewise send coarse local signals. Legacy memory, handoff, artifact, and proxy classification states that still require free-form text fall back to their deterministic/helper behavior. A failed or uncertain metadata-only answer also uses that baseline. No effect or token saving is attributed to a rejected state.

jev.dispatch (issue #744) sends only a bounded numeric row derived locally from the dispatch's own brief: byte length, line count, a count of path-like tokens, counts for two locally-detected keyword classes (hard: debug/race/ concurrency/deadlock/security/architecture/design/migration; mechanical: rename/format/typo/bulk/move/lookup/list), a code-fence count, and a seat-tier index -- never the brief text itself. It never rewrites an explicit model, a named custom subagent_type, or a dispatch whose subagent_type/description names an independent review (that model is the roster's own choice, not this advisory's). A decisive answer records a tier_selected effect naming the chosen tier and the actual model alias; this is a cost-routing lever, not a token-saving one -- moving spend to a cheaper tier lowers price per token, but the child dispatch's own token usage and monetary cost are unknown at hook time (the child has not run yet) and are never invented or recorded as zero. With jev.context active, task-aware optional skill descriptions are selected only for a launch that carries a full skill index (or for a native task); wrapped worker and single-seat launches keep their fixed skill pointer. All implicit skill IDs and load routes remain available. Optional parent-report prose may be omitted only behind the same gate; dependency IDs remain.

The token-savings gates record actions actually taken in the private state directory's jev-effects.jsonl: rendered bytes removed, a helper call skipped, an optional plan seat omitted, a reviewer launch reused, or an eligible retry blocked. A plan omission is not itself a worker launch saved. Token usage is included only when observed, with cache classes separate; unknown usage is omitted. The decision and spend logs remain separate. jev.review_reuse is separate from jev.review: it may reuse a completed round only when its semantic dedup actually converged, the fingerprint, HEAD, tree, reviewer runtime/agent/model and required distinct-reviewer evidence still match, and every open finding has been disposed. Run zirv workflow review run --fresh to force a new round. Judge advice needs local green-gate and successful tool-result evidence; after two consecutive advised skips, the helper judge runs. Crash advice sees repeatable failure signals, and review advice sees severity/category/overlap numbers, never the raw reason, finding text or path.

jev.harvest_screen may skip an optional durable-memory-harvest generation call before it is made: the durable-harvest distiller itself, and, at a clean session exit, the handoff distillation that exists solely to feed it. It sees only local counts -- content size bucket, item count, duplicate ratio against the existing memory bank, existing entry count, path-like token count, and durable-fact-shape line count -- never the handoff text itself. Material with an explicit-remember marker or overlap with an entry already protected as source: explicit, and a clean-exit harvest with repeated tool errors, is never even asked about; generation always runs for it. (Restart-seam harvests carry no tool-error list, so only the marker and explicit-entry guards apply there.) Otherwise, only a decisive "no new durable knowledge" answer skips generation; a disabled gate, a missing credential, or an uncertain/partial/failed answer all fall back to running generation exactly as before.

jev.admin_dispatch (issue #745) answers a small closed set of already-authorized, read-only administrative requests entirely in-process, on Claude Code's UserPromptSubmit hook (zirv ctx hook prompt) -- the one seam that can return {"decision":"block", "reason":...} and stop a prompt before it ever reaches a model, which is the actual LLM-turn saving here (codex and other adapters are untouched). Active only when this gate is on AND the [proxy.typesafe] credential is present; either being false leaves the hook's output byte-identical to today. The prompt is normalized (trimmed, lowercased, whitespace-collapsed, one leading / stripped) and matched by exact string equality against a closed set -- zirv status/zirv ctx status (zirv ctx status's own renderer), zirv jev status (jev::status), and zirv inbox (a mail PEEK that never consumes the message) -- with no arguments accepted anywhere; a near-miss, extra words, or an argument falls straight through to today's path. Because of the #746 egress boundary (jev.rs::safe_metadata_request), no prompt text may ever reach Jev, so this path never makes a Jev call at all -- selection is deterministic exact-match only. A match records one admin_dispatch/llm_turn_avoided effect row (never a decision or cache row); the rendered output is truncated to 8 KiB and prefixed with a line naming the operation. A renderer failure falls back to today's path; the hook never fails a prompt.

jev.classify (issue #782) refines zirv workflow classify's output, and zirv workflow start's pack selection, with one batched Jev call, sending only local facts derived from the request text and the deterministic classifier's own output: intent/ complexity/risk as ids, a word-count bucket, a path-like token count, stated-outcome/stated-constraint flags, and per-domain keyword hit counts for the same six domains the harness proxy already asks about (security, data, docs, dev-ops, architecture, frontend) -- never the request text itself. It asks the same intent options the proxy's own intake asks, plus (for classify, which has an ExecutionProfile to add a tag to) one yes/no question per domain; start classifies before an ExecutionProfile exists, so it asks and applies intent only. Never asks about complexity (a 2026-09-18 replay found complexity answers stable-but-wrong at that margin, routing 1.7-2.2x more spend through Substantial with no accuracy gain). A decisive answer may replace intent outright, or (for classify) ADD a domain tag (security also raises independent_review/security_review) -- it never removes a tag the keyword path already found and never touches risk. A disabled gate, a missing credential, or a failed/uncertain call leaves the deterministic output byte-identical to today.

Handoffs, sockets, logs and scoring checkpoints live in the platform state directory under zirv/ctx/, never in the repo. Override with ZIRV_CTX_STATE_DIR. On unix the state directory is created 0700 and its files 0600: it holds transcript paths, prompts and distilled handoffs. The Stop hook is a fresh process on every turn, so it leaves its parse position and the scoring state derived from it in scoring/, which is what keeps per-turn scoring proportional to the turn rather than to the whole session. Any doubt about a checkpoint -- a rewritten or truncated transcript, changed scoring config, an unreadable file -- silently rebuilds it from a full parse. See Usage pacing below for the [pace] table that governs subscription-window waiting.

Headless cost levers#

Operator-only cost levers for the Claude Code sessions zirv launches HEADLESSLY (-p/--print) -- ctx exec's --prompt path and its -- claude -p ... passthrough, plus zirv agent claude headless workers (they share ctx exec's own launch builder). Interactive wrap/chat/dash sessions never read this table.

Effort defaults to low for every class (measured against Claude Code's medium over 10 XL tasks x 4 runs x 2 rounds: cost -10%, agent time -9%, hidden-test pass rate +1 pt, blind judge -1.75 pts); set a class to medium to restore Claude Code's own default. The cache TTL defaults to 5m: measured 2026-10-06 (macOS, sonnet, 10 XL tasks x 2 reps per arm, two independent rounds, every request gap under 5 minutes), it cut cost by 18% (17.8% and 18.0%; cache writes bill at 1.25x input instead of 2x), and repricing 222 recorded headless sessions gave -17% to -22% with no session losing. The trade-off: a headless session that idles more than 5 minutes between turns (a tool call running longer than 5 minutes, or a --resume follow-up after a pause) re-writes its whole cache at that turn, so set "1h" (Claude Code's own default) for such workloads. lean is on by default: measured 2026-10-06 (macOS, sonnet, 10 XL tasks x 2 reps per arm, two rounds), cost -9.6% at equal cache pricing (-12.9% and -7.2%), cache-write tokens -15%, hidden tests and blind judge unchanged. It turns off auto-memory, Claude Code's bundled skills and the Workflow, ScheduleWakeup, ShareOnboardingGuide, ListAgents and ReportFindings tools for headless launches, and the Agent tool too when the launch's prompt role may not spawn workers (worker and single; orchestrator and sub-orchestrator keep it); set lean = false to restore them. disallowed_tools is empty by default. The example below shows every key; its bounded/substantial effort values (medium) are overrides, not the defaults:

toml
[headless]
prompt_cache_ttl = "5m"          # "5m" | "1h", "5m" by default; ZIRV_CTX_HEADLESS_PROMPT_CACHE_TTL -- skipped when the operator's own env already sets CLAUDE_CODE_PROMPT_CACHE_TTL/FORCE_PROMPT_CACHING_5M/ENABLE_PROMPT_CACHING_1H
lean = true                      # true by default; false restores auto-memory, bundled skills and the tools below. Adds "autoMemoryEnabled": false and "disableBundledSkills": true to the launch settings layer and denies the `Workflow` tool (else Claude Code inlines its authoring guide, ~6k tokens) plus `ScheduleWakeup`, `ShareOnboardingGuide`, `ListAgents`, `ReportFindings`, and `Agent` for worker/single roles; ZIRV_CTX_HEADLESS_LEAN
disallowed_tools = []            # extra tool names appended to the launch's --disallowedTools; ZIRV_CTX_HEADLESS_DISALLOWED_TOOLS (comma-separated)

[headless.effort]
trivial = "low"                  # low | medium | high | xhigh | max, "low" by default for all three classes; ZIRV_CTX_HEADLESS_EFFORT_TRIVIAL
bounded = "medium"               # ZIRV_CTX_HEADLESS_EFFORT_BOUNDED
substantial = "medium"           # ZIRV_CTX_HEADLESS_EFFORT_SUBSTANTIAL -- also what an Architectural-complexity request reads; see below

prompt_cache_ttl sets env CLAUDE_CODE_PROMPT_CACHE_TTL on the child; it does not affect native sessions (see [runtime] prompt_cache_ttl). The effort table classifies the prompt text with the SAME deterministic classifier the intake hook uses (proxy::decision::try_classify_request, text-only by default -- see jev.launch_effort below for the opt-in exception) and sets env CLAUDE_CODE_EFFORT_LEVEL when the resolved complexity has a configured value; an operator's own CLAUDE_CODE_EFFORT_LEVEL or an argv that already carries --effort wins over it. lean and disallowed_tools only ever narrow a HEADLESS launch -- an interactive session, where a human is present, is untouched. With every key unset (the shipped default) a headless launch differs from one built before this table existed only by CLAUDE_CODE_EFFORT_LEVEL=low, CLAUDE_CODE_PROMPT_CACHE_TTL=5m and the lean tool/settings surface. A launch with no prompt text to classify uses the effort shared by all three classes.

The classifier sees the request text only, so the class follows its size unless its wording classifies higher: 120 or more words, or 3 or more list items, is bounded; 300 or more words, or 8 or more items, is substantial; anything shorter is trivial. There is no architectural key: the same text-only classifier can never return that complexity (it needs real changed paths/lines to justify), so a request that would otherwise classify architectural reads the substantial value instead. The default 5m TTL suits headless runs whose turns are seconds apart; a session that idles longer than five minutes between turns re-writes its cache at that turn (set "1h").

The effort decision is made ONCE, from the FIRST headless launch of a conversation, and every later launch of that SAME session -- a --resume, an in-place compaction, any other relaunch that keeps the id -- reuses it regardless of its own prompt text, including a bare resume with no new prompt at all. Changing CLAUDE_CODE_EFFORT_LEVEL mid-conversation invalidates Claude's whole prompt cache, not just that turn's own addition to it, so re-classifying every launch independently was actively counter-productive.

jev.launch_effort (off by default) may RAISE that FIRST launch's pick: when the gate is on and Jev is available (same [proxy.typesafe] credential every other [jev] site shares), a metadata-only Noul question asks whether the request is unusually hard, deliberate work, from local numeric facts only -- prompt word-count bucket, enumerated-item count, the deterministic classifier's own complexity index, and whether the prompt reads as a question -- never the prompt text itself. Only a decisive yes selects headless.effort.substantial; Jev can never lower effort, because "not unusually hard" is no evidence of a small follow-up. A decisive no, an indecisive answer, a failed call, an unavailable credential, or a decisive yes whose substantial tier has no configured value all keep the plain deterministic classification above, exactly as with the gate off. With the all-low default the gate changes nothing until substantial is set higher. Whichever value wins goes through the SAME sticky, per-session record as the deterministic path: a resumed session never re-asks and never changes effort mid-conversation, and this can never override an explicit --effort or the operator's own CLAUDE_CODE_EFFORT_LEVEL (both checks happen before the sticky decision is even reached).

[jev.floors.<site>] (issue #803, off by default -- unset is byte-identical to today's compiled constants) tunes min_confidence/ min_margin per site for the eight TUNABLE sites above: context, harvest_screen, handoff_select, compaction_select, dispatch, launch_effort, classify, inject. Every safety/verification site (approve, approve_allow, inject_screen, stop_verify, missing_tests, retry, review, gates) and the harness proxy's own intake thresholds (proxy.min_confidence/proxy.min_margin) are NOT configurable here and keep their own compiled floors. [jev.floors] is REPO_FORBIDDEN as a whole table, the same trust asymmetry as [jev] itself -- see the config reference table above.

Approvals inbox#

Operator-only, off by default (see the dashboard for the strip and its keys):

toml
[approvals]
inbox = true        # hold Claude PermissionRequests for the owning dashboard; ZIRV_CTX_APPROVALS_INBOX
hold_secs = 300     # 1-570; the native dialog shows after this long; the hook timeout is hold_secs + 30; ZIRV_CTX_APPROVALS_HOLD_SECS

Each held request also carries display-only details for the dashboard to show: command (the full command or input summary, redacted, at most 600 characters), cwd, reason (Claude's Bash description, or the reason line of a Codex dialog), outside_sandbox (Claude dangerouslyDisableSandbox, or a Codex permissions dialog) and always (a short label for the always-allow rule the harness itself offers, null when it offers none). Every field is redacted, flattened and capped again when the dashboard receives it.

Always allow is a third answer beside allow and deny, with every guard of allow (the request the last frame drew, shown in full). On Claude the hook echoes one entry of the payload's own permission_suggestions (the first addRules entry with behavior: "allow") as hookSpecificOutput.decision.updatedPermissions: [<that entry>], per the hooks reference. zirv never builds or widens a rule: a mode change, a directory grant or a deny suggestion is never offered, and with no suggestion the key is refused and the native dialog stays. The dashboard sends only the word allow_always; the rule is whatever Claude suggested, written to the destination Claude named. Codex draws a "Yes, and don't ask again for ..." option, which the dialog reader reports (with its key) as always; no Codex request is listed or answered by the inbox yet.

Both keys are REPO_FORBIDDEN: a repository cannot switch the inbox on or stretch a hold. With inbox = false (the default), or with no live dashboard serving the session, the hook behaves exactly as before and adds no latency.

Tool-output compaction#

zirv setup installs the claude PostToolUse hook: a large Bash result is replaced with a compact summary before the model ever sees it, and the original stays retrievable in full with zirv ctx output show <id>. The size thresholds are output.compact_min_bytes (4096 bytes, for a known build/test/VCS tool) and output.compact_generic_min_bytes (16384 bytes, for everything else); a unified diff (git diff/show/log -p/ format-patch) instead gets output.diff_max_bytes (65536 bytes) and a bounded per-file listing rather than a head/tail. rg/grep/find/fd/ ls/dir/tree results past the generic threshold get a grouped, capped rendering by default too (output.compact_search); set it to false to leave those seven verbatim at any size instead.

Once a Generic-scope result is past its threshold, a bundled [[output.filter]] rule set (output.filter_defaults, on by default) strips common noise -- progress bars, download/package-manager chatter -- before the head/tail summary ever runs over it:

RuleCommands matchedStrips
pkg-pythonpip/pipx/uv/poetry/pdm/conda/mambaCollecting/Downloading/wheel-build lines, progress bars
pkg-systemapt/dnf/yum/apk/pacman/zypper/brew/choco/winget/scoopPackage-list/unpack/setup progress lines, progress bars
download-progresscurl/wget/Invoke-WebRequest/iwr/aria2cTransfer-meter header/rows, progress bars
build-stepscmake/ninja/meson/bazel/buck[n/N]/[n%] build-step lines, progress bars
toolchain-installrustup/nvm/fnm/volta/pyenv/rbenv/asdf/miseinfo: downloading/installing/... lines, progress bars
infra-refreshterraform/tofu/terragrunt/pulumi/cdkRefreshing state.../Still creating... lines, progress bars
test-runner-jsjest/vitest/mocha/ava/tap/karma/playwright/cypress/deno test/bun testPassing (✓/PASS/ok N -) lines
progress-noiseevery other command (catch-all)Progress-bar lines only

The first matching rule (in that order) wins. Declare a [[output.filter]] rule with the same name in ~/.zirv/ctx.toml to replace one of these outright -- an operator rule always wins over a bundled rule sharing its name:

toml
# ~/.zirv/ctx.toml
[[output.filter]]
name = "pkg-python"
match_command = "^(pip|uv|poetry)\\b"
strip_lines = ["^\\s*Downloading "]

Set output.filter_defaults = false to drop the bundled rules entirely and keep only your own.

Trust boundary#

SurfaceTrustNarrowing?
Scratchpad roots and shell/query exceptionstrusted harness environment and built-in classificationno repository setting can expand the roots or bypass an explicit ask/deny rule
Native release availabilityfixed in the binaryno flag, configuration, environment variable or Cargo feature can enable native execution
~/.zirv/ZIRV.md, ~/CLAUDE.md, ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md (operator-global)operatorn/a — operator-authored
<repo>/ZIRV.md, <repo>/.zirv/ZIRV.md, nested ZIRV.md, AGENTS.md, CLAUDE.md, singular AGENT.md (repo-owned, any scope)repo-owned, untrustednarrows only — read as prose context, never as authority
.zirv/ctx.toml with a repo-forbidden key, seen by a PreToolUse Bash/PowerShell hook or the zirv ctx safety check hookrepo-owned and untrustedthe refusal never silences the safety check: the hook evaluates with built-in, operator (~/.zirv/ctx.toml) and ZIRV_CTX_* layers only, names the unapplied repo config in its reason, and asks if even that cannot load; zirv ctx status still reports the refusal and exits nonzero, and a launch still refuses; any other config error still fails open
[[workspace]] in .zirv/ctx.tomlrepo-owned and untrustedadditive-only inert name/mcp_servers/skills; git and setup are rejected because selecting a name is not operator authorization
[[workspace]] in ~/.zirv/ctx.tomloperatormay additionally declare bounded, environment-scrubbed git clones and setup commands; every materialization finishes before worker launch
ZIRV_CTX_OBFUSCATE_MODEoperator environmentselects off, flag or obfuscate; no repository equivalent
ZIRV_CTX_OBFUSCATE_ENTROPYoperator environmentselects whether heuristic entropy findings are flagged or masked
ZIRV_CTX_OBFUSCATE_PROMPToperator environmentselects flag or block for typed prompts that hooks cannot rewrite
ZIRV_CTX_OBFUSCATE_EMAIL_DOMAINoperator environmentselects whether an email placeholder retains its domain; a repository may only narrow to mask
prompt.intake_disciplineoperator home or environment; repository may narrowa repository may only turn the first-prompt discipline note off, never back on for an operator who disabled it
Approvals inbox "always allow" (^A Y)operator, by key on a request the dashboard drew in fullapplies only a permission_suggestions entry Claude itself sent for that call (an allow-rule addition); a repository, a hook payload field, mail, the CLI and MCP have no way to choose or trigger it, and a request carrying no suggestion refuses it
[jev] token-savings gatesoperator home or environment onlyoff by default; each site also needs the named nonempty TypeSafe credential, or a live relay to the supervisor that holds it (scrubbed exec and dashboard Worker sessions), before reading cached advice or writing Jev records; repository/model-authored material may only remove optional context or prevent a permitted launch, never grant or waive a required check
Native /settings writesoperator, by keyboard in the panethe same validation and atomic write as zirv ctx config; session scope is an in-memory, journaled override of live-reload keys only and never touches a file; project scope refuses every REPO_FORBIDDEN key and any value that would not narrow before writing, credential-like keys are never rendered, journaled or accepted
[sandbox] scrub_worker_secretsoperator home or environment onlyon by default; a delegated worker (zirv agent, zirv ctx exec/loop) launches without secret-shaped environment variables, never a repository's call to turn off; its hook-side [jev] gates reach Jev through the supervisor's relay (exec, dashboard Worker panes), and a loop cycle with a gate on keeps credential_env
[headless] cost leversoperator home or environment onlya headless (-p) Claude Code launch only -- prompt-cache TTL, per-complexity effort (defaults to low) and a lean/--disallowedTools tool surface (lean on by default) -- with every key unset the launch differs from before this table existed only by CLAUDE_CODE_EFFORT_LEVEL=low, CLAUDE_CODE_PROMPT_CACHE_TTL=5m and the lean settings layer and unused-tool (Workflow, ScheduleWakeup, ShareOnboardingGuide, ListAgents, ReportFindings, Agent for worker/single) deny; an interactive wrap/chat/dash session is never narrowed by it
[models] discovery, price refresh, pins, avoid and auto_avoidoperator home or environment onlydiscovery/refresh default on, avoid empty and auto_avoid off; the scorecard only reads zirv's own logs; reads account-local caches/transcripts, while network access occurs only in zirv ctx models refresh, run explicitly or as the detached background refresh started by status and dashboard startup; repositories cannot select or conceal the operator's models or prices
[policy] network_allowlistoperator (home layer, or the same operator-owned repo layer's own narrowing)a repository checkout may only remove hosts from the operator's own list, never name one beyond it — naming an ungranted host is a hard error; on Claude Code, a non-empty list replaces the wholesale WebFetch/WebSearch allow in the launch argv with one WebFetch(domain:<host>)/WebSearch(domain:<host>) allow rule per host (reported degraded, never enforced) — it scopes those two brokered tools only, and does nothing to Bash network calls (curl, wget, a raw socket, or any other network-capable program); an operator-only [sandbox] extra_allow entry naming bare WebFetch or WebSearch is appended afterwards and re-widens it
Autoresearch campaign manifest (zirv workflow research plan|run) and every file it references (corpus, fixture, evaluator, candidate patch)operator input, like a scripta [[candidates]] env overlay may only use a key that is in BOTH the manifest's own [candidate_space] allow_env AND a compiled-in allowlist (non-safety Jev gates/floors, ZIRV_CTX_PROXY_MIN_CONFIDENCE/MIN_MARGIN, the handover ladder, [headless] effort, [score] token ratios); anything permission/sandbox/safety/credential/base_url-shaped, and the fixed Jev safety gates (approve/approve_allow/inject_screen/stop_verify/missing_tests/review/gates/admin_dispatch), are always refused regardless of what the manifest declares; repo-owned .zirv/ files cannot widen either list, and a requires_receipts entry outside the compiled prefixes is refused at plan time

Every native instruction file inside the repository checkout — ZIRV.md (root, .zirv/ fallback, or nested), AGENTS.md, CLAUDE.md, and the singular AGENT.md compatibility alias — is RepoUntrusted: it can steer a session's prose but can never widen sandboxing, approvals, credentials, provider/account/billing routing, tool grants, workflow policy floors, or settings precedence, the same asymmetry REPO_FORBIDDEN enforces for ctx.toml below. Only the operator-global ~/.zirv/ZIRV.md (and its CLAUDE.md/AGENTS.md counterparts) carries Operator trust, and even then only as prose context — instructions are context, never permissions, at every scope.

A repository config is part of a checkout, so cloning a repository must not be enough to change what zirv executes. <repo>/.zirv/ctx.toml may not set agent, agent_bin, supervise.on_failure, handoff.model, optimize.model, sandbox.enabled, prompt.enabled, prompt.repo_layer, prompt.max_repo_bytes, prompt.harnesses, prompt.codex_orchestrator, prompt.skill_index_repo_filter, prompt.verbosity, chat.claude_permission_mode, mail.enabled, mail.max_delivered_bytes, mail.mid_turn, any supervisor.* key, chrome.events, any memory.* key, any dash.* key, any pace.* key, any price.* key, any models.* key, any proxy.* key, any jev.* key, any headless.* key, review, worker.claude, worker.codex, worker.codex_effort, worker.default_depth, worker.default_read_only, worker.bootstrap_timeout_secs, handover, obfuscate.mode, obfuscate.entropy, obfuscate.prompt, obfuscate.allow, obfuscate.literals_file, any session.* key, any runtime.* key, or any of the five keys that feed the token gate (score.token_floor, score.token_ceiling, score.token_floor_ratio, score.token_ceiling_ratio, score.model_context_tokens); doing so is an error that names the key. Set those in ~/.zirv/ctx.toml, or with the matching ZIRV_CTX_* variable below, which comes from the operator rather than the checkout:

The same narrowing-only rule applies to native execution. Native tools receive the already-resolved policy and resource claims from trusted runtime state; repository instructions and model output cannot add roots, provider credentials, network targets, approvals, or a different seat generation. Process environment overrides are part of the broker-signed action and may not replace protected credential variables. Declared process effects only request additional sandbox access; under-declaring an effect leaves that resource read-only or disconnected rather than bypassing policy.

The native agent loop adds no repository-settable key either. Which runtime runs, which provider route it spends, which role it holds, which session it resumes and whether a fixture transport stands in for a real provider all come from the command line and from the operator-owned native provider configuration in ~/.zirv/native.toml — never from a checkout. Model output is untrusted input throughout: a tool call it emits is admitted by the shared before-tool service and the N04 broker before anything runs, and its "I am finished" token is one input to the final status rather than the answer. A repository can still narrow, through the same [supervise] orchestrator_writes posture that governs the harness path, which the native loop applies to its own file_write/apply_patch calls.

Native workers and the delegation tools add no repository-settable key either. Which runtime a worker runs on, which route it spends and which task it claims come from the command line (or from the parent's own delegation tool call), never from a checkout. A delegation tool call is model output and is treated as such: it crosses the broker as an ExecutionAction::Delegate under the same seat generation fence every other native tool runs behind, the delegation handle it names is validated as [A-Za-z0-9_-]{1,128} before it can reach a file, and the worker it asks for is narrowed against the parent's own envelope (--path-scope, --no-network, --depth, writing vs read-only) — asking for more than the parent holds is refused, never clamped. A nested worker gets its own principal and one hop less delegation depth, so it inherits none of the parent's session authority. The delegation record itself lives under the operator-owned state directory, never in the repository.

Native workflows, verification and helper calls add no repository-settable key either. Which runtime a workflow reviewer, agent seat or script agent: step runs on comes from the command line or the script the operator wrote; which route a helper spends comes from the operator-owned [roles] table in ~/.zirv/native.toml, and a role with no entry has no native path at all. A helper session is constructed with no writer permit, so its read-only character is the broker's decision at effect time rather than a prompt a model could be talked out of, and workflow_advance/workflow_approve are priced as shared-scope writes for the same reason. The workflow id a tool call names is validated as [A-Za-z0-9_-]{1,128} before it can reach a file, so model output cannot address state outside the workflow store. The completion gate reads the workflow's own recorded branch and the verification store, never the model's account of them.

The native meta-orchestrator adds no repository-settable key either. A seat's role comes from the persisted seat record zirv itself minted, never from model output, and that role alone decides whether the seat may delegate and whether a child it delegates may write — so a read-only coordinator can still dispatch a writing implementer, and a reviewer stays read-only however the request was spelled. A route a delegating model names for a role is admitted only when it clears operator policy and stays on the billing posture the operator seated that role on; an operator's own --route is the operator speaking and is untouched. Task and group ids a tool call names are validated as [A-Za-z0-9_-]{1,128} before they can reach a file, and task_create/task_claim/group_create are priced as shared-scope writes, so a seat with no writer permit reads the board and cannot move a piece on it. The coordinator's own record lives under the operator-owned state directory, never in the repository.

The local runtime protocol (zirv ctx api) adds no configuration key, and deliberately so: its endpoint is always derived from the operator-owned state directory, there is no setting or flag that names one, and the server's policy — which methods exist, which capabilities are advertised, who may connect — is compiled into the binary. A repository therefore has nothing to narrow here, and nothing to widen either.

Forbidden repo keySet instead via
agentZIRV_CTX_AGENT
agent_binZIRV_CTX_AGENT_BIN
supervise.on_failureZIRV_CTX_ON_FAILURE
handoff.modelZIRV_CTX_MODEL
optimize.modelZIRV_CTX_OPTIMIZE_MODEL
sandbox.enabledZIRV_CTX_SANDBOX
sandbox.extra_allowZIRV_CTX_SANDBOX_EXTRA_ALLOW
sandbox.scrub_subprocess_envZIRV_CTX_SANDBOX_SCRUB_SUBPROCESS_ENV
sandbox.scrub_worker_secretsZIRV_CTX_SANDBOX_SCRUB_WORKER_SECRETS
prompt.enabledZIRV_CTX_PROMPT
prompt.repo_layerZIRV_CTX_PROMPT_REPO
prompt.max_repo_bytesZIRV_CTX_PROMPT_MAX_REPO_BYTES
prompt.harnessesZIRV_CTX_PROMPT_HARNESSES
prompt.codex_orchestratorZIRV_CTX_PROMPT_CODEX_ORCHESTRATOR
prompt.skill_index_repo_filterZIRV_CTX_PROMPT_SKILL_INDEX_REPO_FILTER
prompt.verbosityZIRV_CTX_PROMPT_VERBOSITY
chat.claude_permission_modeZIRV_CTX_CHAT_CLAUDE_PERMISSION_MODE
context.max_common_bytesZIRV_CTX_CONTEXT_MAX_COMMON_BYTES
context.max_harness_bytesZIRV_CTX_CONTEXT_MAX_HARNESS_BYTES
context.max_harness_roster_bytesZIRV_CTX_CONTEXT_MAX_HARNESS_ROSTER_BYTES
context.instructions_max_bytesZIRV_CTX_CONTEXT_INSTRUCTIONS_MAX_BYTES
context.lint_max_pairsZIRV_CTX_CONTEXT_LINT_MAX_PAIRS
mail.enabledZIRV_CTX_MAIL
mail.max_delivered_bytesZIRV_CTX_MAIL_MAX_DELIVERED_BYTES
mail.mid_turnZIRV_CTX_MAIL_MID_TURN
supervisor.enabledZIRV_CTX_SUPERVISOR_ENABLED
supervisor.harnessZIRV_CTX_SUPERVISOR_HARNESS
supervisor.modelZIRV_CTX_SUPERVISOR_MODEL
supervisor.max_callsZIRV_CTX_SUPERVISOR_MAX_CALLS
supervisor.max_advice_bytesZIRV_CTX_SUPERVISOR_MAX_ADVICE_BYTES
chrome.eventsZIRV_CTX_QUIET (see the note below on why this one's name looks different)
memory.enabledZIRV_CTX_MEMORY
memory.harvestZIRV_CTX_MEMORY_HARVEST
memory.max_entriesZIRV_CTX_MEMORY_MAX_ENTRIES
memory.max_entry_bytesZIRV_CTX_MEMORY_MAX_ENTRY_BYTES
memory.max_injected_bytesZIRV_CTX_MEMORY_MAX_INJECTED_BYTES
memory.shared_enabledZIRV_CTX_MEMORY_SHARED
memory.core_max_bytesZIRV_CTX_MEMORY_CORE_MAX_BYTES
memory.retrieval_max_bytesZIRV_CTX_MEMORY_RETRIEVAL_MAX_BYTES
memory.retrieval_max_entriesZIRV_CTX_MEMORY_RETRIEVAL_MAX_ENTRIES
memory.harvest_max_entriesZIRV_CTX_MEMORY_HARVEST_MAX_ENTRIES
memory.harvest_max_bytesZIRV_CTX_MEMORY_HARVEST_MAX_BYTES
memory.session_enabledZIRV_CTX_MEMORY_SESSION
memory.journal_max_entriesZIRV_CTX_MEMORY_JOURNAL_MAX_ENTRIES
task.max_parent_outcome_bytesZIRV_CTX_TASK_MAX_PARENT_OUTCOME_BYTES
dash.enabledZIRV_CTX_DASH
dash.sidebar_colsZIRV_CTX_DASH_SIDEBAR_COLS
dash.roster_max_age_secsZIRV_CTX_DASH_ROSTER_MAX_AGE_SECS
dash.max_panesZIRV_CTX_DASH_MAX_PANES
dash.mouseZIRV_CTX_DASH_MOUSE
dash.workdir_rootsZIRV_CTX_DASH_WORKDIR_ROOTS
supervise.max_heavy_workersZIRV_CTX_SUPERVISE_MAX_HEAVY_WORKERS (deprecated alias for max_heavy_operations)
supervise.max_heavy_operationsZIRV_CTX_SUPERVISE_MAX_HEAVY_OPERATIONS
supervise.max_writersZIRV_CTX_SUPERVISE_MAX_WRITERS
supervise.idle_no_tool_secsZIRV_CTX_SUPERVISE_IDLE_NO_TOOL_SECS
supervise.in_tool_secsZIRV_CTX_SUPERVISE_IN_TOOL_SECS
supervise.stall_grace_secsZIRV_CTX_SUPERVISE_STALL_GRACE_SECS
supervise.compact_stall_secsZIRV_CTX_SUPERVISE_COMPACT_STALL_SECS
supervise.compact_timeout_msZIRV_CTX_SUPERVISE_COMPACT_TIMEOUT_MS
supervise.chain_max_restartsZIRV_CTX_SUPERVISE_CHAIN_MAX_RESTARTS
supervise.chain_max_gap_secsZIRV_CTX_SUPERVISE_CHAIN_MAX_GAP_SECS
pace.use_creditsZIRV_CTX_PACE_USE_CREDITS_CLAUDE (the table-node match also blocks pace.use_credits.codex alone)
pace.poll_enabledZIRV_CTX_PACE_POLL
pace.poll_min_interval_secsZIRV_CTX_PACE_POLL_MIN_INTERVAL_SECS
pace.blind_delay_secsZIRV_CTX_PACE_BLIND_DELAY_SECS
pace.spawn_soft_pctZIRV_CTX_PACE_SPAWN_SOFT_PCT
pace.spawn_hard_pctZIRV_CTX_PACE_SPAWN_HARD_PCT
pace.run_budget_tokensZIRV_CTX_PACE_RUN_BUDGET_TOKENS
pace.estimatorZIRV_CTX_PACE_ESTIMATOR
pace.collector_max_age_secsZIRV_CTX_PACE_COLLECTOR_MAX_AGE_SECS
pace.five_hour_budget_tokensZIRV_CTX_FIVE_HOUR_BUDGET
pace.seven_day_budget_tokensZIRV_CTX_SEVEN_DAY_BUDGET
pace.count_cache_readsZIRV_CTX_PACE_COUNT_CACHE_READS
review (review.claude, review.codex)ZIRV_CTX_REVIEW_MODEL_CLAUDE / ZIRV_CTX_REVIEW_MODEL_CODEX
worker.claudeZIRV_CTX_WORKER_MODEL_CLAUDE
worker.codexZIRV_CTX_WORKER_MODEL_CODEX
worker.codex_effortZIRV_CTX_WORKER_EFFORT_CODEX (Codex worker reasoning effort, e.g. low/medium/high; passed as -c model_reasoning_effort=; an operator -c model_reasoning_effort=... passthrough wins; unset leaves Codex's own default)
worker.default_depthZIRV_CTX_WORKER_DEFAULT_DEPTH
worker.default_read_onlyZIRV_CTX_WORKER_DEFAULT_READ_ONLY
worker.bootstrap_timeout_secsZIRV_CTX_WORKER_BOOTSTRAP_TIMEOUT_SECS (whole goal-bootstrap run; default 600, must be greater than zero)
handover (handover.<agent>.<tier>)ZIRV_CTX_HANDOVER_<AGENT>_<TIER> (e.g. ZIRV_CTX_HANDOVER_CLAUDE_DEEP)
model_tiers (model_tiers.<agent>.<tier>)ZIRV_CTX_MODEL_TIERS_<AGENT>_<TIER> (e.g. ZIRV_CTX_MODEL_TIERS_CLAUDE_DEEP)
endpoint (endpoint.claude, endpoint.codex)none -- ~/.zirv/ctx.toml only, chooses which vendor account a seat spends
route.<id>.execution~/.zirv/native.toml only; selects an official provider process and optional absolute executable path. Repository layers cannot select executables, login methods, billing or startup settings; all effects retain the native broker
Claude Code authentication environment and public user settingsUser-owned process environment and ~/.claude/settings.json (or an absolute CLAUDE_CONFIG_DIR outside the repository); only authentication settings are carried into the restricted model invocation. Official login receives options after --. Inherited auth values are excluded from persisted settings and diagnostic output
native.toml keys other than policy.allowed_routes, policy.compaction and policy.context_editing~/.zirv/native.toml only; repository allowed_routes is intersected with the operator set, repository compaction may only narrow automatic to advisory, and repository context_editing may only narrow true to false -- never the reverse for either
safety.allowZIRV_CTX_SAFETY_ALLOW
safety.escape_allowZIRV_CTX_SAFETY_ESCAPE_ALLOW
safety.defaultZIRV_CTX_SAFETY_DEFAULT
safety.interactive_defaultZIRV_CTX_SAFETY_INTERACTIVE_DEFAULT
safety.sqlZIRV_CTX_SAFETY_SQL
score.token_floorZIRV_CTX_TOKEN_FLOOR
score.token_ceilingZIRV_CTX_TOKEN_CEILING
score.token_floor_ratioZIRV_CTX_SCORE_TOKEN_FLOOR_RATIO
score.token_ceiling_ratioZIRV_CTX_SCORE_TOKEN_CEILING_RATIO
score.model_context_tokensZIRV_CTX_SCORE_MODEL_CONTEXT_TOKENS
workflow.repo_checks_enabledZIRV_CTX_WORKFLOW_REPO_CHECKS
workflow.repo_skills_enabledZIRV_CTX_WORKFLOW_REPO_SKILLS
workflow.repo_agents_enabledZIRV_CTX_WORKFLOW_REPO_AGENTS
workflow.repo_workflows_enabledZIRV_CTX_WORKFLOW_REPO_WORKFLOWS
workflow.deploy.tierZIRV_CTX_WORKFLOW_DEPLOY_TIER
workflow.adoptionZIRV_CTX_WORKFLOW_ADOPTION
workflow.auto_startZIRV_CTX_WORKFLOW_AUTO_START
workflow.maintain~/.zirv/ctx.toml only
report.repositoryZIRV_CTX_REPORT_REPOSITORY
workflow.telemetry_enabledZIRV_CTX_WORKFLOW_TELEMETRY
workflow.telemetry_max_eventsZIRV_CTX_WORKFLOW_TELEMETRY_MAX_EVENTS
workflow.telemetry_retention_daysZIRV_CTX_WORKFLOW_TELEMETRY_RETENTION_DAYS
workflow.check_env_passthroughZIRV_CTX_WORKFLOW_CHECK_ENV_PASSTHROUGH
workflow.review_worker_budget_tokensZIRV_CTX_WORKFLOW_REVIEW_WORKER_BUDGET_TOKENS
workflow.review_worker_max_tool_callsZIRV_CTX_WORKFLOW_REVIEW_WORKER_MAX_TOOL_CALLS
workflow.auto_spawn_on_gateZIRV_CTX_WORKFLOW_AUTO_SPAWN_ON_GATE
workflow.allow_empty_verifyZIRV_CTX_WORKFLOW_ALLOW_EMPTY_VERIFY
workflow.builtin_checks_excludeZIRV_CTX_WORKFLOW_BUILTIN_CHECKS_EXCLUDE
workflow.max_context_bytesZIRV_CTX_WORKFLOW_MAX_CONTEXT_BYTES
price.stale_after_daysZIRV_CTX_PRICE_STALE_AFTER_DAYS
price.table_pathZIRV_CTX_PRICE_TABLE_PATH
models.discoveryZIRV_CTX_MODELS_DISCOVERY
models.price_fetchZIRV_CTX_MODELS_PRICE_FETCH
models.pinZIRV_CTX_MODELS_PIN (vendor.family=model, comma-separated)
models.avoidZIRV_CTX_MODELS_AVOID (model ids, comma-separated)
models.auto_avoidZIRV_CTX_MODELS_AUTO_AVOID
modelsZIRV_CTX_MODELS_* (the table-node match also blocks any other models.* key)
search.max_output_bytesZIRV_CTX_SEARCH_MAX_OUTPUT_BYTES
output.compactZIRV_CTX_OUTPUT_COMPACT
output.compact_min_bytesZIRV_CTX_OUTPUT_COMPACT_MIN_BYTES
output.compact_generic_min_bytesZIRV_CTX_OUTPUT_COMPACT_GENERIC_MIN_BYTES
output.verbatimZIRV_CTX_OUTPUT_VERBATIM
output.max_summary_bytesZIRV_CTX_OUTPUT_MAX_SUMMARY_BYTES
output.compact_searchZIRV_CTX_OUTPUT_COMPACT_SEARCH
output.filter~/.zirv/ctx.toml only
output.filter_defaultsZIRV_CTX_OUTPUT_FILTER_DEFAULTS
fallback.orchestrator_rollover_headroom_pctZIRV_CTX_FALLBACK_ORCHESTRATOR_ROLLOVER_HEADROOM_PCT
fallback.rollover_cooldown_secs (also the base delay for failed rollover retries)ZIRV_CTX_FALLBACK_ROLLOVER_COOLDOWN_SECS
fallback.reactive_force_after_secsZIRV_CTX_FALLBACK_REACTIVE_FORCE_AFTER_SECS
fallback.health.open_after_failuresZIRV_CTX_FALLBACK_HEALTH_OPEN_AFTER_FAILURES
fallback.health.window_secsZIRV_CTX_FALLBACK_HEALTH_WINDOW_SECS
fallback.health.cooldown_secsZIRV_CTX_FALLBACK_HEALTH_COOLDOWN_SECS
fallback.health.degrade_error_rate_pctZIRV_CTX_FALLBACK_HEALTH_DEGRADE_ERROR_RATE_PCT
fallback.health.degrade_min_samplesZIRV_CTX_FALLBACK_HEALTH_DEGRADE_MIN_SAMPLES
fallback.health.degrade_ttft_msZIRV_CTX_FALLBACK_HEALTH_DEGRADE_TTFT_MS
session.persistentZIRV_CTX_SESSION_PERSISTENT
session.historyZIRV_CTX_SESSION_HISTORY
session.scrollback_rowsZIRV_CTX_SESSION_SCROLLBACK_ROWS
session.stale_after_secsZIRV_CTX_SESSION_STALE_AFTER_SECS
proxy.enabledZIRV_CTX_PROXY_ENABLED
proxy.deciderZIRV_CTX_PROXY_DECIDER
proxy.min_confidenceZIRV_CTX_PROXY_MIN_CONFIDENCE
proxy.min_marginZIRV_CTX_PROXY_MIN_MARGIN
proxy.request_max_bytesZIRV_CTX_PROXY_REQUEST_MAX_BYTES
proxy.validation_gateZIRV_CTX_PROXY_VALIDATION_GATE
proxy.typesafe.base_urlZIRV_CTX_PROXY_TYPESAFE_BASE_URL
proxy.typesafe.credential_envZIRV_CTX_PROXY_TYPESAFE_CREDENTIAL_ENV
proxy.typesafe.modelZIRV_CTX_PROXY_TYPESAFE_MODEL
proxy.typesafe.timeout_secsZIRV_CTX_PROXY_TYPESAFE_TIMEOUT_SECS
jev.memoryZIRV_CTX_JEV_MEMORY
jev.supervisorZIRV_CTX_JEV_SUPERVISOR
jev.dispatchZIRV_CTX_JEV_DISPATCH
jev.reviewZIRV_CTX_JEV_REVIEW
jev.gatesZIRV_CTX_JEV_GATES
jev.contextZIRV_CTX_JEV_CONTEXT
jev.intake_savingsZIRV_CTX_JEV_INTAKE_SAVINGS
jev.review_reuseZIRV_CTX_JEV_REVIEW_REUSE
jev.harvest_screenZIRV_CTX_JEV_HARVEST_SCREEN
jev.admin_dispatchZIRV_CTX_JEV_ADMIN_DISPATCH
jev.approveZIRV_CTX_JEV_APPROVE
jev.approve_allowZIRV_CTX_JEV_APPROVE_ALLOW
jev.classifyZIRV_CTX_JEV_CLASSIFY
jev.handoff_selectZIRV_CTX_JEV_HANDOFF_SELECT
jev.compaction_selectZIRV_CTX_JEV_COMPACTION_SELECT
jev.inject_screenZIRV_CTX_JEV_INJECT_SCREEN
jev.injectZIRV_CTX_JEV_INJECT
jev.stop_verifyZIRV_CTX_JEV_STOP_VERIFY
jev.missing_testsZIRV_CTX_JEV_MISSING_TESTS
jev.launch_effortZIRV_CTX_JEV_LAUNCH_EFFORT
jev.retryZIRV_CTX_JEV_RETRY
jev.cache_ttl_secsZIRV_CTX_JEV_CACHE_TTL_SECS
jev.floorsZIRV_CTX_JEV_FLOOR_<SITE>_MIN_CONFIDENCE|_MIN_MARGIN
headless.prompt_cache_ttlZIRV_CTX_HEADLESS_PROMPT_CACHE_TTL
headless.effort.trivialZIRV_CTX_HEADLESS_EFFORT_TRIVIAL
headless.effort.boundedZIRV_CTX_HEADLESS_EFFORT_BOUNDED
headless.effort.substantialZIRV_CTX_HEADLESS_EFFORT_SUBSTANTIAL
headless.leanZIRV_CTX_HEADLESS_LEAN
headless.disallowed_toolsZIRV_CTX_HEADLESS_DISALLOWED_TOOLS
approvals.inboxZIRV_CTX_APPROVALS_INBOX
approvals.hold_secsZIRV_CTX_APPROVALS_HOLD_SECS
obfuscate.modeZIRV_CTX_OBFUSCATE_MODE
obfuscate.entropyZIRV_CTX_OBFUSCATE_ENTROPY
obfuscate.promptZIRV_CTX_OBFUSCATE_PROMPT
obfuscate.allow~/.zirv/ctx.toml only
obfuscate.literals_file~/.zirv/ctx.toml only
capabilitiesZIRV_CTX_CAPABILITIES
runtimeZIRV_CTX_RUNTIME

capabilities is listed as a whole table rather than key by key: every key under it names an MCP server command zirv spawns, a remote endpoint it authenticates to, a credential reference, or a browser binary it launches, so there is no narrowing half a repository checkout could legitimately set.

runtime is a whole table for the same reason, in both directions: it decides which provider account a session with no explicit --runtime spends, and a checkout moving that onto the operator's metered native routes — or off them — is widening either way. ZIRV_CTX_RUNTIME sets runtime.default (harness or native); per-role overrides live in [runtime.roles] in ~/.zirv/ctx.toml. See Native setup, diagnosis and rollback.

The mail.*/chrome.events entries close the same hole prompt.max_repo_bytes does: mail is folded into a launched worker's prompt as its own layer, so a repo raising its own delivered-mail cap (or turning delivery back on after an operator disabled it) would make the operator's choice decorative; a repo silencing the announcement channel would hide its own degradation notices from anyone running zirv there. prompt.harnesses closes the same loop for the derived per-adapter harness-roster layer: a repo checkout must not be able to force that layer back on for an operator who turned it off. prompt.codex_orchestrator closes the same loop once more for codex's own orchestrator-conventions layer (issue #167): a repo checkout must not be able to re-enable it for an operator who turned it off. prompt.verbosity (issue #427) closes the same loop for the Orchestrator-only meta-harness orientation layer's own named tier: a repo checkout must not be able to raise the tier back up for an operator who chose a lower one. session.* closes a sharper version of the same hole: session.persistent decides whether cloning a repository is enough to make sessions started from it outlive the operator's terminal, and session.history decides whether rendered terminal output — tokens, keys and file contents included — is written to disk at all. Neither is a decision a checkout gets to make. memory.* closes the same hole again for the memory bank's configuration (not its content -- see below): a repo checkout must not be able to switch either scope's gate on or off for itself, raise its own caps, or switch automatic harvesting on for anyone who runs zirv there (see Memory bank below -- the shared scope's whole point is that its content is deliberately repo-committed, but a checkout still may not flip its own shared_enabled gate any more than the private scope's enabled gate). dash.* closes it once more for the session multiplexer zirv chat opens on a capable terminal: a repo checkout must not be able to switch it on or off, resize its sidebar, change how long a quit-time restore roster stays offered, raise its own pane cap, or decide whether the dashboard captures the mouse. pace.* closes it for usage pacing: a repo must not be able to flip a spend decision, re-enable the active vendor-API poll fallback an operator turned off, or change its cadence. review/worker close it for which model spends the operator's tokens running background review or delegated-worker sessions. handover closes the same hole for zirv ctx handover: a repo checkout must not be able to pick which harness or model the orchestrator seat swaps onto mid-session. agent closes a narrower hole discovered once codex shipped out of the box: an explicit agent = "codex" reaches resolve_default's configured arm, which never consults the repo-narrowing guard the no-agent-configured fallback loop has (see .settings.toml below) -- without this, a repo checkout could pick which vendor account gets spent with that guard never in the way. workflow.adoption closes the same hole once more for the workflow-adoption nudge/enforce gate (issue #223): a repo checkout must not be able to turn its own adoption pressure down to off, or up to enforce to hold an operator's own agent dispatches hostage. workflow.auto_start is operator-only for the same reason: a repo must not start workflows, with their approvals and spend, on the operator's behalf. fallback.orchestrator_rollover_headroom_pct/fallback.rollover_cooldown_secs/fallback.reactive_force_after_secs (issue #358) close the same hole for automatic orchestrator-seat rollover: the on/off switch (fallback.auto_orchestrator_rollover) stays repo-narrowable like fallback.enabled, but tuning when an already-enabled rollover fires or how soon another one may follow picks the same kind of vendor-spend decision handoff.model/optimize.model already gate, so only the operator may set either. fallback.health.open_after_failures/fallback.health.window_secs/fallback.health.cooldown_secs (issue #455) close it once more for health-aware routing: how many failures make zirv stop trusting a vendor route, over what window, and how long it stays distrusted decides where the operator's tokens get spent, so only they may tune it. fallback.health.degrade_error_rate_pct/fallback.health.degrade_min_samples/fallback.health.degrade_ttft_ms are the same decision one step earlier -- how bad a route has to get before zirv starts ranking it behind the operator's other account -- and are operator-only for the same reason. fallback.health.enabled itself stays repo-narrowable like fallback.enabled — a checkout may switch the breaker off, never on. Everything else, including chrome.banner/chrome.bar, supervise.max_nudges, and every threshold, is still repo-configurable.

Environment variables worth knowing#

VariableEffect
ZIRV_CTX_STATE_DIRWhere handoffs, sockets, logs and usage state live
ZIRV_CTX_TRANSCRIPTPins the transcript wrap watches, overriding what the turn signal reports (see below)
ZIRV_CTX_SOCKET, ZIRV_CTX_SESSIONExported into the supervised agent so its hook can find the supervisor. Set by zirv, not by you
ZIRV_AGENT_<NAME>_ENABLEDEnables or disables one adapter (ZIRV_AGENT_CODEX_ENABLED); see .settings.toml below. Must be exactly true or false -- any other value is a hard error naming the variable, matching the strictness of every ZIRV_CTX_* boolean

Every [section] key in the tables above also has a ZIRV_CTX_* variable; the names follow the key, for example ZIRV_CTX_DEBOUNCE_MS for wrap.debounce_ms, with two deliberate exceptions: a section's own top-level enabled flag drops the _ENABLED suffix (ZIRV_CTX_MAIL for mail.enabled, ZIRV_CTX_PROMPT for prompt.enabled, and so on), and ZIRV_CTX_QUIET is a named alias for chrome.events set to its opposite (ZIRV_CTX_QUIET=true turns events off) rather than ZIRV_CTX_CHROME_EVENTS, because "quiet" is the more natural spelling for the flag most people will actually reach for.