DocsSupervision (zirv ctx)
Configuration
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.
# .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_SECSEach 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:
[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 belowprompt_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):
[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_SECSEach 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:
| Rule | Commands matched | Strips |
|---|---|---|
pkg-python | pip/pipx/uv/poetry/pdm/conda/mamba | Collecting/Downloading/wheel-build lines, progress bars |
pkg-system | apt/dnf/yum/apk/pacman/zypper/brew/choco/winget/scoop | Package-list/unpack/setup progress lines, progress bars |
download-progress | curl/wget/Invoke-WebRequest/iwr/aria2c | Transfer-meter header/rows, progress bars |
build-steps | cmake/ninja/meson/bazel/buck | [n/N]/[n%] build-step lines, progress bars |
toolchain-install | rustup/nvm/fnm/volta/pyenv/rbenv/asdf/mise | info: downloading/installing/... lines, progress bars |
infra-refresh | terraform/tofu/terragrunt/pulumi/cdk | Refreshing state.../Still creating... lines, progress bars |
test-runner-js | jest/vitest/mocha/ava/tap/karma/playwright/cypress/deno test/bun test | Passing (✓/PASS/ok N -) lines |
progress-noise | every 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:
# ~/.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#
| Surface | Trust | Narrowing? |
|---|---|---|
| Scratchpad roots and shell/query exceptions | trusted harness environment and built-in classification | no repository setting can expand the roots or bypass an explicit ask/deny rule |
| Native release availability | fixed in the binary | no flag, configuration, environment variable or Cargo feature can enable native execution |
~/.zirv/ZIRV.md, ~/CLAUDE.md, ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md (operator-global) | operator | n/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, untrusted | narrows 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 hook | repo-owned and untrusted | the 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.toml | repo-owned and untrusted | additive-only inert name/mcp_servers/skills; git and setup are rejected because selecting a name is not operator authorization |
[[workspace]] in ~/.zirv/ctx.toml | operator | may additionally declare bounded, environment-scrubbed git clones and setup commands; every materialization finishes before worker launch |
ZIRV_CTX_OBFUSCATE_MODE | operator environment | selects off, flag or obfuscate; no repository equivalent |
ZIRV_CTX_OBFUSCATE_ENTROPY | operator environment | selects whether heuristic entropy findings are flagged or masked |
ZIRV_CTX_OBFUSCATE_PROMPT | operator environment | selects flag or block for typed prompts that hooks cannot rewrite |
ZIRV_CTX_OBFUSCATE_EMAIL_DOMAIN | operator environment | selects whether an email placeholder retains its domain; a repository may only narrow to mask |
prompt.intake_discipline | operator home or environment; repository may narrow | a 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 full | applies 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 gates | operator home or environment only | off 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 writes | operator, by keyboard in the pane | the 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_secrets | operator home or environment only | on 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 levers | operator home or environment only | a 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_avoid | operator home or environment only | discovery/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_allowlist | operator (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 script | a [[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 key | Set instead via |
|---|---|
agent | ZIRV_CTX_AGENT |
agent_bin | ZIRV_CTX_AGENT_BIN |
supervise.on_failure | ZIRV_CTX_ON_FAILURE |
handoff.model | ZIRV_CTX_MODEL |
optimize.model | ZIRV_CTX_OPTIMIZE_MODEL |
sandbox.enabled | ZIRV_CTX_SANDBOX |
sandbox.extra_allow | ZIRV_CTX_SANDBOX_EXTRA_ALLOW |
sandbox.scrub_subprocess_env | ZIRV_CTX_SANDBOX_SCRUB_SUBPROCESS_ENV |
sandbox.scrub_worker_secrets | ZIRV_CTX_SANDBOX_SCRUB_WORKER_SECRETS |
prompt.enabled | ZIRV_CTX_PROMPT |
prompt.repo_layer | ZIRV_CTX_PROMPT_REPO |
prompt.max_repo_bytes | ZIRV_CTX_PROMPT_MAX_REPO_BYTES |
prompt.harnesses | ZIRV_CTX_PROMPT_HARNESSES |
prompt.codex_orchestrator | ZIRV_CTX_PROMPT_CODEX_ORCHESTRATOR |
prompt.skill_index_repo_filter | ZIRV_CTX_PROMPT_SKILL_INDEX_REPO_FILTER |
prompt.verbosity | ZIRV_CTX_PROMPT_VERBOSITY |
chat.claude_permission_mode | ZIRV_CTX_CHAT_CLAUDE_PERMISSION_MODE |
context.max_common_bytes | ZIRV_CTX_CONTEXT_MAX_COMMON_BYTES |
context.max_harness_bytes | ZIRV_CTX_CONTEXT_MAX_HARNESS_BYTES |
context.max_harness_roster_bytes | ZIRV_CTX_CONTEXT_MAX_HARNESS_ROSTER_BYTES |
context.instructions_max_bytes | ZIRV_CTX_CONTEXT_INSTRUCTIONS_MAX_BYTES |
context.lint_max_pairs | ZIRV_CTX_CONTEXT_LINT_MAX_PAIRS |
mail.enabled | ZIRV_CTX_MAIL |
mail.max_delivered_bytes | ZIRV_CTX_MAIL_MAX_DELIVERED_BYTES |
mail.mid_turn | ZIRV_CTX_MAIL_MID_TURN |
supervisor.enabled | ZIRV_CTX_SUPERVISOR_ENABLED |
supervisor.harness | ZIRV_CTX_SUPERVISOR_HARNESS |
supervisor.model | ZIRV_CTX_SUPERVISOR_MODEL |
supervisor.max_calls | ZIRV_CTX_SUPERVISOR_MAX_CALLS |
supervisor.max_advice_bytes | ZIRV_CTX_SUPERVISOR_MAX_ADVICE_BYTES |
chrome.events | ZIRV_CTX_QUIET (see the note below on why this one's name looks different) |
memory.enabled | ZIRV_CTX_MEMORY |
memory.harvest | ZIRV_CTX_MEMORY_HARVEST |
memory.max_entries | ZIRV_CTX_MEMORY_MAX_ENTRIES |
memory.max_entry_bytes | ZIRV_CTX_MEMORY_MAX_ENTRY_BYTES |
memory.max_injected_bytes | ZIRV_CTX_MEMORY_MAX_INJECTED_BYTES |
memory.shared_enabled | ZIRV_CTX_MEMORY_SHARED |
memory.core_max_bytes | ZIRV_CTX_MEMORY_CORE_MAX_BYTES |
memory.retrieval_max_bytes | ZIRV_CTX_MEMORY_RETRIEVAL_MAX_BYTES |
memory.retrieval_max_entries | ZIRV_CTX_MEMORY_RETRIEVAL_MAX_ENTRIES |
memory.harvest_max_entries | ZIRV_CTX_MEMORY_HARVEST_MAX_ENTRIES |
memory.harvest_max_bytes | ZIRV_CTX_MEMORY_HARVEST_MAX_BYTES |
memory.session_enabled | ZIRV_CTX_MEMORY_SESSION |
memory.journal_max_entries | ZIRV_CTX_MEMORY_JOURNAL_MAX_ENTRIES |
task.max_parent_outcome_bytes | ZIRV_CTX_TASK_MAX_PARENT_OUTCOME_BYTES |
dash.enabled | ZIRV_CTX_DASH |
dash.sidebar_cols | ZIRV_CTX_DASH_SIDEBAR_COLS |
dash.roster_max_age_secs | ZIRV_CTX_DASH_ROSTER_MAX_AGE_SECS |
dash.max_panes | ZIRV_CTX_DASH_MAX_PANES |
dash.mouse | ZIRV_CTX_DASH_MOUSE |
dash.workdir_roots | ZIRV_CTX_DASH_WORKDIR_ROOTS |
supervise.max_heavy_workers | ZIRV_CTX_SUPERVISE_MAX_HEAVY_WORKERS (deprecated alias for max_heavy_operations) |
supervise.max_heavy_operations | ZIRV_CTX_SUPERVISE_MAX_HEAVY_OPERATIONS |
supervise.max_writers | ZIRV_CTX_SUPERVISE_MAX_WRITERS |
supervise.idle_no_tool_secs | ZIRV_CTX_SUPERVISE_IDLE_NO_TOOL_SECS |
supervise.in_tool_secs | ZIRV_CTX_SUPERVISE_IN_TOOL_SECS |
supervise.stall_grace_secs | ZIRV_CTX_SUPERVISE_STALL_GRACE_SECS |
supervise.compact_stall_secs | ZIRV_CTX_SUPERVISE_COMPACT_STALL_SECS |
supervise.compact_timeout_ms | ZIRV_CTX_SUPERVISE_COMPACT_TIMEOUT_MS |
supervise.chain_max_restarts | ZIRV_CTX_SUPERVISE_CHAIN_MAX_RESTARTS |
supervise.chain_max_gap_secs | ZIRV_CTX_SUPERVISE_CHAIN_MAX_GAP_SECS |
pace.use_credits | ZIRV_CTX_PACE_USE_CREDITS_CLAUDE (the table-node match also blocks pace.use_credits.codex alone) |
pace.poll_enabled | ZIRV_CTX_PACE_POLL |
pace.poll_min_interval_secs | ZIRV_CTX_PACE_POLL_MIN_INTERVAL_SECS |
pace.blind_delay_secs | ZIRV_CTX_PACE_BLIND_DELAY_SECS |
pace.spawn_soft_pct | ZIRV_CTX_PACE_SPAWN_SOFT_PCT |
pace.spawn_hard_pct | ZIRV_CTX_PACE_SPAWN_HARD_PCT |
pace.run_budget_tokens | ZIRV_CTX_PACE_RUN_BUDGET_TOKENS |
pace.estimator | ZIRV_CTX_PACE_ESTIMATOR |
pace.collector_max_age_secs | ZIRV_CTX_PACE_COLLECTOR_MAX_AGE_SECS |
pace.five_hour_budget_tokens | ZIRV_CTX_FIVE_HOUR_BUDGET |
pace.seven_day_budget_tokens | ZIRV_CTX_SEVEN_DAY_BUDGET |
pace.count_cache_reads | ZIRV_CTX_PACE_COUNT_CACHE_READS |
review (review.claude, review.codex) | ZIRV_CTX_REVIEW_MODEL_CLAUDE / ZIRV_CTX_REVIEW_MODEL_CODEX |
worker.claude | ZIRV_CTX_WORKER_MODEL_CLAUDE |
worker.codex | ZIRV_CTX_WORKER_MODEL_CODEX |
worker.codex_effort | ZIRV_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_depth | ZIRV_CTX_WORKER_DEFAULT_DEPTH |
worker.default_read_only | ZIRV_CTX_WORKER_DEFAULT_READ_ONLY |
worker.bootstrap_timeout_secs | ZIRV_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 settings | User-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.allow | ZIRV_CTX_SAFETY_ALLOW |
safety.escape_allow | ZIRV_CTX_SAFETY_ESCAPE_ALLOW |
safety.default | ZIRV_CTX_SAFETY_DEFAULT |
safety.interactive_default | ZIRV_CTX_SAFETY_INTERACTIVE_DEFAULT |
safety.sql | ZIRV_CTX_SAFETY_SQL |
score.token_floor | ZIRV_CTX_TOKEN_FLOOR |
score.token_ceiling | ZIRV_CTX_TOKEN_CEILING |
score.token_floor_ratio | ZIRV_CTX_SCORE_TOKEN_FLOOR_RATIO |
score.token_ceiling_ratio | ZIRV_CTX_SCORE_TOKEN_CEILING_RATIO |
score.model_context_tokens | ZIRV_CTX_SCORE_MODEL_CONTEXT_TOKENS |
workflow.repo_checks_enabled | ZIRV_CTX_WORKFLOW_REPO_CHECKS |
workflow.repo_skills_enabled | ZIRV_CTX_WORKFLOW_REPO_SKILLS |
workflow.repo_agents_enabled | ZIRV_CTX_WORKFLOW_REPO_AGENTS |
workflow.repo_workflows_enabled | ZIRV_CTX_WORKFLOW_REPO_WORKFLOWS |
workflow.deploy.tier | ZIRV_CTX_WORKFLOW_DEPLOY_TIER |
workflow.adoption | ZIRV_CTX_WORKFLOW_ADOPTION |
workflow.auto_start | ZIRV_CTX_WORKFLOW_AUTO_START |
workflow.maintain | ~/.zirv/ctx.toml only |
report.repository | ZIRV_CTX_REPORT_REPOSITORY |
workflow.telemetry_enabled | ZIRV_CTX_WORKFLOW_TELEMETRY |
workflow.telemetry_max_events | ZIRV_CTX_WORKFLOW_TELEMETRY_MAX_EVENTS |
workflow.telemetry_retention_days | ZIRV_CTX_WORKFLOW_TELEMETRY_RETENTION_DAYS |
workflow.check_env_passthrough | ZIRV_CTX_WORKFLOW_CHECK_ENV_PASSTHROUGH |
workflow.review_worker_budget_tokens | ZIRV_CTX_WORKFLOW_REVIEW_WORKER_BUDGET_TOKENS |
workflow.review_worker_max_tool_calls | ZIRV_CTX_WORKFLOW_REVIEW_WORKER_MAX_TOOL_CALLS |
workflow.auto_spawn_on_gate | ZIRV_CTX_WORKFLOW_AUTO_SPAWN_ON_GATE |
workflow.allow_empty_verify | ZIRV_CTX_WORKFLOW_ALLOW_EMPTY_VERIFY |
workflow.builtin_checks_exclude | ZIRV_CTX_WORKFLOW_BUILTIN_CHECKS_EXCLUDE |
workflow.max_context_bytes | ZIRV_CTX_WORKFLOW_MAX_CONTEXT_BYTES |
price.stale_after_days | ZIRV_CTX_PRICE_STALE_AFTER_DAYS |
price.table_path | ZIRV_CTX_PRICE_TABLE_PATH |
models.discovery | ZIRV_CTX_MODELS_DISCOVERY |
models.price_fetch | ZIRV_CTX_MODELS_PRICE_FETCH |
models.pin | ZIRV_CTX_MODELS_PIN (vendor.family=model, comma-separated) |
models.avoid | ZIRV_CTX_MODELS_AVOID (model ids, comma-separated) |
models.auto_avoid | ZIRV_CTX_MODELS_AUTO_AVOID |
models | ZIRV_CTX_MODELS_* (the table-node match also blocks any other models.* key) |
search.max_output_bytes | ZIRV_CTX_SEARCH_MAX_OUTPUT_BYTES |
output.compact | ZIRV_CTX_OUTPUT_COMPACT |
output.compact_min_bytes | ZIRV_CTX_OUTPUT_COMPACT_MIN_BYTES |
output.compact_generic_min_bytes | ZIRV_CTX_OUTPUT_COMPACT_GENERIC_MIN_BYTES |
output.verbatim | ZIRV_CTX_OUTPUT_VERBATIM |
output.max_summary_bytes | ZIRV_CTX_OUTPUT_MAX_SUMMARY_BYTES |
output.compact_search | ZIRV_CTX_OUTPUT_COMPACT_SEARCH |
output.filter | ~/.zirv/ctx.toml only |
output.filter_defaults | ZIRV_CTX_OUTPUT_FILTER_DEFAULTS |
fallback.orchestrator_rollover_headroom_pct | ZIRV_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_secs | ZIRV_CTX_FALLBACK_REACTIVE_FORCE_AFTER_SECS |
fallback.health.open_after_failures | ZIRV_CTX_FALLBACK_HEALTH_OPEN_AFTER_FAILURES |
fallback.health.window_secs | ZIRV_CTX_FALLBACK_HEALTH_WINDOW_SECS |
fallback.health.cooldown_secs | ZIRV_CTX_FALLBACK_HEALTH_COOLDOWN_SECS |
fallback.health.degrade_error_rate_pct | ZIRV_CTX_FALLBACK_HEALTH_DEGRADE_ERROR_RATE_PCT |
fallback.health.degrade_min_samples | ZIRV_CTX_FALLBACK_HEALTH_DEGRADE_MIN_SAMPLES |
fallback.health.degrade_ttft_ms | ZIRV_CTX_FALLBACK_HEALTH_DEGRADE_TTFT_MS |
session.persistent | ZIRV_CTX_SESSION_PERSISTENT |
session.history | ZIRV_CTX_SESSION_HISTORY |
session.scrollback_rows | ZIRV_CTX_SESSION_SCROLLBACK_ROWS |
session.stale_after_secs | ZIRV_CTX_SESSION_STALE_AFTER_SECS |
proxy.enabled | ZIRV_CTX_PROXY_ENABLED |
proxy.decider | ZIRV_CTX_PROXY_DECIDER |
proxy.min_confidence | ZIRV_CTX_PROXY_MIN_CONFIDENCE |
proxy.min_margin | ZIRV_CTX_PROXY_MIN_MARGIN |
proxy.request_max_bytes | ZIRV_CTX_PROXY_REQUEST_MAX_BYTES |
proxy.validation_gate | ZIRV_CTX_PROXY_VALIDATION_GATE |
proxy.typesafe.base_url | ZIRV_CTX_PROXY_TYPESAFE_BASE_URL |
proxy.typesafe.credential_env | ZIRV_CTX_PROXY_TYPESAFE_CREDENTIAL_ENV |
proxy.typesafe.model | ZIRV_CTX_PROXY_TYPESAFE_MODEL |
proxy.typesafe.timeout_secs | ZIRV_CTX_PROXY_TYPESAFE_TIMEOUT_SECS |
jev.memory | ZIRV_CTX_JEV_MEMORY |
jev.supervisor | ZIRV_CTX_JEV_SUPERVISOR |
jev.dispatch | ZIRV_CTX_JEV_DISPATCH |
jev.review | ZIRV_CTX_JEV_REVIEW |
jev.gates | ZIRV_CTX_JEV_GATES |
jev.context | ZIRV_CTX_JEV_CONTEXT |
jev.intake_savings | ZIRV_CTX_JEV_INTAKE_SAVINGS |
jev.review_reuse | ZIRV_CTX_JEV_REVIEW_REUSE |
jev.harvest_screen | ZIRV_CTX_JEV_HARVEST_SCREEN |
jev.admin_dispatch | ZIRV_CTX_JEV_ADMIN_DISPATCH |
jev.approve | ZIRV_CTX_JEV_APPROVE |
jev.approve_allow | ZIRV_CTX_JEV_APPROVE_ALLOW |
jev.classify | ZIRV_CTX_JEV_CLASSIFY |
jev.handoff_select | ZIRV_CTX_JEV_HANDOFF_SELECT |
jev.compaction_select | ZIRV_CTX_JEV_COMPACTION_SELECT |
jev.inject_screen | ZIRV_CTX_JEV_INJECT_SCREEN |
jev.inject | ZIRV_CTX_JEV_INJECT |
jev.stop_verify | ZIRV_CTX_JEV_STOP_VERIFY |
jev.missing_tests | ZIRV_CTX_JEV_MISSING_TESTS |
jev.launch_effort | ZIRV_CTX_JEV_LAUNCH_EFFORT |
jev.retry | ZIRV_CTX_JEV_RETRY |
jev.cache_ttl_secs | ZIRV_CTX_JEV_CACHE_TTL_SECS |
jev.floors | ZIRV_CTX_JEV_FLOOR_<SITE>_MIN_CONFIDENCE|_MIN_MARGIN |
headless.prompt_cache_ttl | ZIRV_CTX_HEADLESS_PROMPT_CACHE_TTL |
headless.effort.trivial | ZIRV_CTX_HEADLESS_EFFORT_TRIVIAL |
headless.effort.bounded | ZIRV_CTX_HEADLESS_EFFORT_BOUNDED |
headless.effort.substantial | ZIRV_CTX_HEADLESS_EFFORT_SUBSTANTIAL |
headless.lean | ZIRV_CTX_HEADLESS_LEAN |
headless.disallowed_tools | ZIRV_CTX_HEADLESS_DISALLOWED_TOOLS |
approvals.inbox | ZIRV_CTX_APPROVALS_INBOX |
approvals.hold_secs | ZIRV_CTX_APPROVALS_HOLD_SECS |
obfuscate.mode | ZIRV_CTX_OBFUSCATE_MODE |
obfuscate.entropy | ZIRV_CTX_OBFUSCATE_ENTROPY |
obfuscate.prompt | ZIRV_CTX_OBFUSCATE_PROMPT |
obfuscate.allow | ~/.zirv/ctx.toml only |
obfuscate.literals_file | ~/.zirv/ctx.toml only |
capabilities | ZIRV_CTX_CAPABILITIES |
runtime | ZIRV_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#
| Variable | Effect |
|---|---|
ZIRV_CTX_STATE_DIR | Where handoffs, sockets, logs and usage state live |
ZIRV_CTX_TRANSCRIPT | Pins the transcript wrap watches, overriding what the turn signal reports (see below) |
ZIRV_CTX_SOCKET, ZIRV_CTX_SESSION | Exported into the supervised agent so its hook can find the supervisor. Set by zirv, not by you |
ZIRV_AGENT_<NAME>_ENABLED | Enables 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.