Configuration
An audit is configured by its flags — every seat is named on the command line, because corral has no default models — and by a handful of environment variables for keys, the sandbox, signing and where files live. There is no config file to hand-edit. Everything below is optional except the seats.
Seats and provider keys
Each seat — --mutant-model, --writer-model, --critic-model, and on
certify --repo --derive-model — resolves its own backend from the name you
give it. A run with an unnamed seat is refused before any sandbox, store or
spend, and the refusal says which provider credentials it can see. The one rule:
the critic must differ from the writer (--critic-model off drops the critic,
which is advisory and never gates).
| Variable | What it is |
|---|---|
ANTHROPIC_API_KEY |
Claude models |
GEMINI_API_KEY (or GOOGLE_API_KEY) |
Gemini models |
OPENAI_API_KEY |
OpenAI models |
OPENROUTER_API_KEY |
OpenRouter, one key for several vendors |
MODEL_BACKEND / OPENAI_BASE_URL |
Pin every seat to one endpoint (OpenRouter, a local daemon) instead of routing by model name. An explicit gateway is never re-routed |
OLLAMA_URL |
The local Ollama daemon (default http://127.0.0.1:11434); --local-endpoint <role>=<url> places one seat on another daemon |
A key alone does not move a seat to a provider. A Gemini model name with only an Anthropic key configured is an error from Anthropic, not a Gemini call: set the model names and the matching keys.
Keys can live in corral’s keystore instead of a plaintext environment:
corral secret set ANTHROPIC_API_KEY # value read from stdin — never a CLI arg
corral secret list # names only, never values
corral secret rm GEMINI_API_KEY
Each secret resolves through env var → OS keyring → an age-encrypted file
(first hit wins, so an env var always overrides a stored value). On a headless
server the store is age-encrypted under your config dir (CORRAL_CREDS_DIR to
override), with its identity from CORRAL_AGE_IDENTITY, a systemd credential
(age-identity) or a 0600 key file, and it fails closed if that source is
present but malformed. Model keys are removed from the environment of your tests
and of everything else corral runs to grade them; an agent seat’s CLI is the
exception (below).
Agent seats (subscriptions)
Any seat can be a coding-agent CLI instead of an API model: claude-code and
codex (and antigravity) come defined, as <agent> or <agent>:<model>.
The agent uses its own login, so on a subscription plan the seat has no
per-token API bill. Define your own:
| Variable | What it is |
|---|---|
CORRALAI_AGENT_<NAME> |
The command line for a seat called <name>, with {dir}, {out}, {model}, {model:FLAG} and {tools:FLAG} substituted |
CORRALAI_AGENT_<NAME>_WIRE |
How to read its reply: claude-json, codex-jsonl or text (the default for your own definitions). A text seat is not metered, and is refused under --max-tokens |
Never put a credential in a CORRALAI_AGENT_* definition — it is published
verbatim in the signed record. The agent CLI inherits corral’s environment, so an
exported vendor API key may bill instead of the subscription. Details, and what
was measured: agent seats.
The model registry
Seats can be named by alias from a registry, read from exactly one of these (most explicit first; they do not merge):
| Source | What it is |
|---|---|
CORRALAI_MODELS |
The whole registry as inline JSON — how a CI job declares a herd without committing a file |
CORRALAI_MODELS_FILE |
A registry file elsewhere on disk (a named file that does not exist is an error) |
.corral/models.json |
The repository’s own registry |
A registry marked "strict": true requires every seat to name a declared alias.
The sandbox
| Variable | What it is |
|---|---|
CORRALAI_EXEC_IMAGE |
The image --jail container runs in; it must carry your toolchain (e.g. python:3.12-bookworm). Required for that backend |
CORRALAI_EXEC_RUNTIME |
Which container runtime to use (default: podman, then docker, whichever is on PATH) |
bwrap is the Linux default and needs nothing configured, except on Ubuntu
24.04 and later, where AppArmor must allow it — see
Getting started. certify --repo --substrate workspace runs on your checkout instead of in a sandbox; see
Trust & security.
Signing, the ledger and the cache
| Variable | What it is | Default |
|---|---|---|
CORRALAI_CERTIFY_KEY |
Hex-encoded Ed25519 seed that signs verdicts and ledger entries; takes priority over the file | — |
CORRALAI_CERTIFY_KEY_FILE |
Where the signing key seed lives; created 0600 on first use by certify --local |
$STATE/corralai_certify_key |
CORRAL_LEDGER |
The ledger directory certify --repo writes its entry to and reads its prior from (--ledger overrides) |
<repo>/.corral/ledger |
CORRALAI_MACHINE_LEDGER |
This machine’s own ledger: certify --local (and so corral demo, which has no opt-out) writes its entry here, and certify --repo and review keep a copy of theirs here (--no-ledger skips it; never written inside the audited checkout) |
$STATE/ledger |
CORRALAI_LEDGER_PUBKEY |
The public key corral ledger verify checks signatures against when --pub is not given |
— (signatures not checked) |
CORRALAI_CACHE_DB |
The local cache of derived goals and coverage selections (a cache, not a record) | $CACHE/corralai_cache.duckdb |
motherduck_token |
Read by --push md:<db> to reach MotherDuck |
— |
corral certify pubkey prints the public half of your signing key. A
certify --repo ledger entry is signed when a key is configured or already
exists at the default path, and written unsigned — and said to be — when none
is. The verdict cache is the ledger directory itself, not a file of its own.
Where state lives
Every local store defaults under corral’s state directory
(~/.local/state/corral on Linux, ~/Library/Application Support/corral on
macOS) and the cache under ~/.cache/corral. CORRALAI_STATE_DIR and
CORRALAI_CACHE_DIR move them; a per-store variable moves one.
corral state paths prints every file, which variable moves it, and where it
resolves. The ones an audit writes are the signing key, and on certify --local
the scorecard stores (CORRALAI_BUGCATCH_DB, CORRALAI_CRITICSCORE_DB,
CORRALAI_MUTANT_ATTEMPTS_DB); the rest belong to the daemon.
Moving from an older install
Before 1.0, corral kept these files in ~/.claude. It no longer reads
that directory, and it refuses to start a store whose only copy is still
there rather than silently create an empty one (or a new signing key).
Run corral state migrate --dry-run, then corral state migrate, with
the daemon stopped. It prints the certify key’s public key before and
after; it must match the LEDGER_PUBKEY of any ledger you sign.
Migrate refuses, touching nothing, on any conflict, on a symlinked source, on
a source owned by another user and on a running daemon. If a migrate stops
partway, fix the cause and run corral state migrate --resume: database side
files move first, and a side file already at the destination is a conflict
unless --resume says it came from that interrupted run (each one adopted is
listed). Do not start the daemon until a migrate completes. To keep using the
old directory in place instead, set CORRALAI_STATE_DIR to it.
The optional daemon
The daemon is configured entirely through environment variables; with none set, it boots in dev mode (auth off) against defaults under the state directory.
| Variable | What it is | Default |
|---|---|---|
CORRALAI_ADDR |
Listen address (loopback by default; front it with a tunnel/proxy) | 127.0.0.1:9019 |
CORRALAI_DB |
Coordination SQLite (missions, tasks, claims) | $STATE/corralai_coord.sqlite3 |
CORRALAI_MEMORY_DB |
Memory DuckDB (the searchable knowledge store) | $STATE/corralai_memory.duckdb |
CORRALAI_MEMORY_DIR |
Where memory entries are written and indexed (markdown files) | $STATE/memory |
CORRALAI_RECORDINGS_DB |
Recordings DuckDB for scrubbed replay exports | $STATE/corralai_recordings.duckdb |
Reliability & stall detection
| Variable | What it is | Default |
|---|---|---|
CORRALAI_TASK_STALL_SECONDS |
How long a claimed-but-untouched task may sit before the brain treats it as stalled and lets the herd reclaim it | 300 |
This is the knob behind graceful degradation over deadlock: a worker that claims a task and then dies (crash, quota blip) must never strand the mission — after the stall window its lease is reclaimable by the rest of the herd.
Auth & access
With CORRALAI_OIDC_ISSUER unset, auth is disabled (dev mode — any caller
is trusted). Set it to any OIDC provider to turn auth on.
| Variable | What it is |
|---|---|
CORRALAI_OIDC_ISSUER / CORRALAI_OIDC_AUDIENCE |
The primary trusted OIDC issuer + expected audience (empty issuer ⇒ auth off) |
CORRALAI_OIDC_CLIENTS |
Extra trusted clients, "issuer|aud,issuer|aud" |
CORRALAI_ALLOWED_PRINCIPALS |
Day-0 seed of member emails (the DB is canonical afterward) |
CORRALAI_ADMIN_PRINCIPALS |
Day-0 seed of superuser emails |
CORRALAI_ALLOWED_HOSTS |
Accepted Host headers (add the public hostname a tunnel exposes) |
CORRALAI_MEMORY_OWNERS |
Emails allowed to read/write memory |
Repo work (jail → PR)
| Variable | What it is |
|---|---|
CORRALAI_GIT_TOKEN |
Forge PAT for clone + PR; empty ⇒ repo engine off unless CORRALAI_REPO_ENABLE=1 |
CORRALAI_FORGES |
Multi-forge config (GitHub / GitLab / Gitea) — each token is only ever used against its own host |
CORRALAI_REPO_WORKSPACE |
Root dir for per-mission working copies |
The token lives only in the brain — it is scrubbed from the environment,
injected into a push URL for one call, and never written to .git/config. See
SECURITY.md’s section on the optional daemon.
Models & the fleet
| Variable | What it is |
|---|---|
MODEL_BACKEND / OPENAI_BASE_URL |
Bring your own model — point at any OpenAI-compatible endpoint (local Ollama to frontier) |
CORRALAI_ADVPOOL_MODELS |
The hosted pool’s herd: mutant-generator=<m>,test-writer=<m>,test-critic=<m> (test-critic=off or omitted leaves the critic unstaffed). Required — see below |
CORRALAI_EMBED_URL |
Embeddings endpoint for reference RAG + semantic code search |
CORRALAI_MOTHERDUCK |
Fleet-sync target (md:<db> or a .duckdb path); empty ⇒ sync off |
CORRALAI_BRAIN_ID |
Tag for this brain’s rows in the fleet (default: hostname) |
The daemon has no default models either. Its hosted adversarial pool used to
cold-start on a hardcoded pair of small local models, which meant a brain nobody
had configured still ran something. It no longer does: with no
CORRALAI_ADVPOOL_MODELS the pool logs DISABLED — no models assigned and is
never registered, and a run started without an assignment is refused. A
malformed CORRALAI_ADVPOOL_MODELS is refused too, rather than quietly falling
back to something you did not choose.
The models are yours to name, from any provider you hold a key for. The only
rule is that the test-critic must differ from the test-writer — that
decorrelation is what the verdict rests on, and it is a property, not a vendor,
so any two distinct models satisfy it. (The critic may also be left unfilled:
test-critic=off. It is advisory and never gates a verdict.)
The herd is the allowlist. The brain’s leaderboard can re-seat a role — but only to a model you named somewhere in the herd, only once that model has at least 5 completed tasks in that role, and only when it outranks your own pick on the same evidence. A model you never named cannot be routed to, whatever a worker reported about it, and the roster each run actually used is logged at run start.
The keystore above serves the daemon too: corral-agent reads
OPENAI_API_KEY / ANTHROPIC_API_KEY / CORRALAI_BRAIN_KEY through it, so they
need never sit in the process environment.
See Running it for dev mode vs. auth-on, and Multi-model herds for the daemon’s herd.