Zurdo reads its configuration from .zurdo/config.toml at the repo root — per repository, not per user. zurdo init writes the default shown below.
The default config
[roles.executor]
provider = "anthropic" # or "codex" or "copilot"
# model is determined by [effort_map.<provider>] below
[roles.analyzer] # required for zurdo analyze / heal
provider = "anthropic"
model = "claude-haiku-4-5"
[effort_map.anthropic]
low = "claude-haiku-4-5"
medium = "claude-sonnet-4-6"
high = "claude-opus-4-7"
[effort_map.codex]
low = "gpt-5.5"
medium = "gpt-5.5"
high = "gpt-5.5"
[effort_map.copilot] # `auto` lets the Copilot CLI pick the model
low = "auto" # concrete dotted ids (e.g. claude-sonnet-4.6)
medium = "auto" # are plan-gated; probe with `zurdo check-models`
high = "auto"
[defaults]
max_attempts = 3 # per-task fallback
max_total_iterations = 0 # 0 = unlimited
parallel_criteria = false # true only if every shell/http criterion is hermetic
analyzer_parallelism = 4 # concurrent analyzer LLM calls (1–16)
[timeouts]
criterion_seconds = 300 # shell + http only
agent_seconds = 1800
[providers.anthropic]
cli = "claude"
extra_args = []
[providers.codex]
cli = "codex"
extra_args = []
[providers.copilot]
cli = "copilot"
extra_args = []
[lumen] # structural code index — off by default
enabled = false
languages = ["rust", "python", "go", "typescript", "javascript"]
max_file_bytes = 2097152
gc_grace_minutes = 10
[experimental] # feature gates — all default false
structural_hints = false # requires [lumen] enabled = true
[vela] # background index watcher — off by default
enabled = false
Roles
Executor is the agent that does the work. It has no fixed model — the model is chosen per task from the effort map, keyed by the task’s **Effort** metadata. Switching provider is a one-line edit to [roles.executor] provider.
Analyzer powers zurdo analyze’s LLM critique of your PRD and zurdo heal’s re-aim proposals. It names a concrete model directly (analysis doesn’t vary by task effort). If you never use analyze or heal, it sits unused.
Reasoner (optional, [roles.reasoner] — same provider/model shape as the analyzer) powers the opt-in reason subsystem’s diagnosis and lesson-extraction calls. When absent, those calls fall back to [roles.analyzer]. Enabling [reason] with neither role configured is a config-load error.
The effort map
[effort_map.<provider>] maps effort labels to model ids per provider. A PRD’s **Effort** value must be a key in the active executor’s map — the labels are not a fixed enum, so you can define your own tiers:
[effort_map.anthropic]
trivial = "claude-haiku-4-5"
normal = "claude-sonnet-4-6"
gnarly = "claude-opus-4-7"
With that map, **Effort**: gnarly is valid and **Effort**: medium is rejected at pre-flight. The default config defines low | medium | high for all three providers, so most users never think about this.
Before a run, zurdo probes each mapped model against its provider CLI (the model probe); an unknown or plan-gated model fails pre-flight with exit 4. Check without running via zurdo check-models, or bypass with --skip-model-check. See Providers.
Defaults and timeouts
| Key | Meaning | Default |
|---|---|---|
defaults.max_attempts |
Per-task attempt budget when the PRD omits **Max-Attempts**. |
3 |
defaults.max_total_iterations |
Cap on agent invocations across the whole run; 0 = unlimited. Overridable with --max-iterations. |
0 |
defaults.parallel_criteria |
Run a task’s criteria concurrently instead of sequentially. Enable only if every shell:/http: criterion is hermetic — parallel non-hermetic checks can interfere with each other. |
false |
defaults.analyzer_parallelism |
Concurrent analyzer LLM calls during zurdo analyze (range 1–16; 1 preserves fully sequential behavior). |
4 |
timeouts.criterion_seconds |
Time limit per shell:/http: hint execution (file/grep checks are local and unbounded). |
300 |
timeouts.agent_seconds |
Time limit per agent invocation when the task omits **Agent-timeout**. |
1800 |
Providers
Each [providers.<name>] block names the CLI binary zurdo shells out to and optional extra_args appended to every invocation. The binary must be on your PATH and authenticated — zurdo holds no credentials of its own. Details on how each CLI is driven are on the Providers page.
Protected paths
An optional [verification] table declares run-wide frozen globs — paths no agent may modify during any task of a run:
[verification]
protected_paths = ["Cargo.lock", "docs/**/*.md"]
These are enforced in union with each task’s **Frozen** metadata: after every iteration, any protected path appearing in the diff against the run-start baseline fails the iteration regardless of criteria results. Patterns are root-anchored; * stays within one path segment, ** crosses directories, negation is not supported. Enforcement requires the baseline capture, so outside a git repo it degrades to a warning. See How it works.
Skills search paths
Skills referenced in a task’s **Skills** metadata are user-managed. Beyond the provider’s native discovery paths (project-scope .claude/skills/ or .agents/skills/, and their global equivalents), you can register extra directories:
[skills]
search_paths = ["tools/skills", "/opt/shared-skills"]
Zurdo checks these at pre-flight (warn-only if a named skill is missing) but never installs or modifies user-managed skills.
Lumen and structural hints
[lumen] controls the optional structural code index at .zurdo/lumen/ — repo-scoped, shared by every PRD:
| Key | Meaning | Default |
|---|---|---|
lumen.enabled |
Build and maintain the index. | false |
lumen.languages |
Languages indexed. Recognized: rust, python, go, typescript, javascript. |
all five |
lumen.max_file_bytes |
Files larger than this are skipped. | 2097152 |
lumen.gc_grace_minutes |
Index generations older than this window become GC-eligible. | 10 |
The experimental [symbol:]/[references:]/[callers:] hint types additionally require the [experimental] structural_hints = true gate; setting the gate while lumen.enabled = false is a config-load error. A PRD with no structural hints never touches the index. The full subsystem — index lifecycle, per-language capabilities, zurdo lumen CLI — is on Structural verification.
The Vela watcher
[vela] configures the optional background watcher that keeps the Lumen index fresh between runs — a freshness optimization, never required for correctness:
| Key | Meaning | Default |
|---|---|---|
vela.enabled |
Auto-start the watcher from structural operations. | false |
vela.idle_minutes |
The daemon shuts itself down after this long with no filesystem events and no client connections. 0 disables the timeout. |
30 |
vela.debounce_ms |
Debounce window coalescing bursty filesystem events. | 200 |
Manage it explicitly with zurdo vela serve|start|stop|status; zurdo lumen status also reports the watcher’s state. Full daemon semantics are on Structural verification.
Reason: diagnosis and lessons
The [reason] table and [roles.reasoner] opt into stall diagnosis and the cross-run lesson library. They are accepted in config but not seeded by zurdo init — the full key reference, defaults, and lifecycle live on Diagnosis & lessons.
Pricing overrides
The cost estimates in the run banner and summary come from a built-in per-model price table covering every default-config model. Optional [pricing.<model>] blocks override it — to correct a stale rate, alias a self-hosted model, or apply a discount — without rebuilding zurdo:
[pricing.claude-sonnet-4-6]
input = 3.00 # USD per million tokens; required
output = 15.00 # required
cache_write = 3.75 # optional
cache_read = 0.30 # optional
Copilot bills in premium requests rather than tokens, so its override is a request rate plus per-model multipliers (default covers auto and any unmapped id):
[pricing.copilot]
request_rate = 0.04
[pricing.copilot.multipliers]
default = 1.0
"claude-haiku-4.5" = 0.33
Negative values are rejected at config load.
Precedence
Command-line flags override config values (e.g. --max-iterations beats defaults.max_total_iterations); PRD task metadata overrides config defaults for that task (**Max-Attempts**, **Agent-timeout**).
Next: Providers