Configuration
Every .zurdo/config.toml table: providers, the effort map, skills, Lumen, MCP, reason and pricing.
zurdo v1.36.0
Zurdo reads .zurdo/config.toml at the repo root, so config is per repository. zurdo init writes a commented default, and zurdo init --sync reports how far yours has drifted after an upgrade; when your config has no [coverage] table, --sync also prints the generated coverage table that you can append by hand.
Precedence
- CLI flag. Wins over everything, for example
--max-iterations. - PRD task metadata.
Max-AttemptsandAgent-timeoutoverride the config for that task. - config.toml. Applies when nothing above sets a value.
Providers and roles
Zurdo drives agents only through their CLIs on your PATH, signed in as you. It uses no SDK and holds no credentials.
| Provider | CLI | Sign in |
|---|---|---|
anthropic | claude (Claude Code) | claude login or ANTHROPIC_API_KEY |
codex | codex (Codex CLI) | codex login or OPENAI_API_KEY |
copilot | copilot (Copilot CLI) | copilot auth login or GITHUB_TOKEN |
[roles.executor]
The provider that does each task's work; the model comes from the effort map.
| Field | Meaning | Default |
|---|---|---|
provider | anthropic, codex or copilot | anthropic |
Switching providers is this one line. It takes no model key.
[roles.executor]
provider = "codex"
[roles.analyzer]
The provider and fixed model behind analyze, analyze --fix, heal and scaffold.
| Field | Meaning | Default |
|---|---|---|
provider | Any configured provider | anthropic |
model | One model id; the effort map doesn't apply | claude-haiku-4-5 |
Analyzer calls get no tools: they only reply. Without this table those four commands exit 3.
[roles.analyzer]
provider = "anthropic"
model = "claude-haiku-4-5"
[roles.reasoner]
The provider and model for stall diagnoses, post-mortems and lesson extraction.
| Field | Meaning | Default |
|---|---|---|
provider | Any configured provider | falls back to [roles.analyzer] |
model | One model id | falls back to [roles.analyzer] |
Not seeded by init. Only [reason] enabled = true makes Zurdo call it.
[roles.reasoner]
provider = "anthropic"
model = "claude-sonnet-4-6"
[effort_map.<provider>]
Maps each task's **Effort** label to a model id for that provider.
| Field | Meaning | Default (anthropic) |
|---|---|---|
low | Model for **Effort**: low | claude-haiku-4-5 |
medium | Model for **Effort**: medium | claude-sonnet-4-6 |
high | Model for **Effort**: high | claude-opus-4-7 |
| any other key | Labels aren't a fixed set; define your own | — |
A task's label must be a key in the active executor's map, or pre-flight rejects it. Keep the key set the same across providers, so a PRD survives a provider switch.
The codex default is gpt-5.5 for all three. The copilot default is auto, which lets the CLI pick. Copilot's dotted ids, like claude-sonnet-4.6, depend on your plan.
Before a run, Zurdo probes each mapped model, and an unknown one fails pre-flight with exit 4. zurdo doctor runs the same probe; the model probe explains it.
[effort_map.anthropic]
trivial = "claude-haiku-4-5"
normal = "claude-sonnet-4-6"
gnarly = "claude-opus-4-7"
[providers.<name>]
Names the CLI binary Zurdo runs for a provider, plus arguments appended to every call.
| Field | Meaning | Default |
|---|---|---|
cli | Binary on PATH, or a wrapper script | claude, codex, copilot |
extra_args | Appended after Zurdo's own arguments | [] |
init writes all three blocks. The CLIs' own docs are authoritative for their flags and models; they change on their own schedules.
[providers.anthropic]
cli = "claude"
extra_args = []
Runs
[defaults]
Run-wide attempt and iteration ceilings, and concurrency.
| Field | Meaning | Default |
|---|---|---|
max_attempts | Attempts per task when the PRD omits **Max-Attempts** | 3 |
max_total_iterations | Agent calls across the run; 0 is unlimited | 0 |
parallel_criteria | Check a task's criteria concurrently | false |
analyzer_parallelism | Concurrent analyzer calls in analyze (1–16) | 4 |
Turn on parallel_criteria only when every shell: and http: criterion is hermetic.
[defaults]
max_attempts = 3
max_total_iterations = 0
parallel_criteria = false
analyzer_parallelism = 4
[timeouts]
Time limits, in seconds, for checks, agent calls and the completion gate.
| Field | Meaning | Default |
|---|---|---|
criterion_seconds | Per shell: or http: hint; a hint's timeout: overrides it | 300 |
agent_seconds | Per agent call when the task omits **Agent-timeout** | 1800 |
completion_seconds | The completion gate; not seeded by init | 900 |
coverage_seconds | Each coverage source's command, in the coverage gate below | 900 |
[timeouts]
criterion_seconds = 300
agent_seconds = 1800
coverage_seconds = 900
[verification]
Run-wide frozen paths, prompt priming, and the run-end completion gate.
| Field | Meaning | Default |
|---|---|---|
protected_paths | Globs no task may change; a change fails the iteration | [] |
protected_path_defaults | Union the five toolchain defaults below into the run-wide set | true |
prime_context | Add an # Evidence Paths section to the executor prompt | true |
completion_command | One shell command run once after every task passes | unset |
env_passthrough | Extra environment variables a [shell:] criterion and completion_command may see, beyond the fixed allowlist | [] |
http_read_limit | How many bytes of an [http:] response body the verifier reads; 0 is rejected | 65536 |
standing | Standing rules checked on every task, written as [[verification.standing]] entries | [] |
standing_budget_seconds | Time limit for the standing-rule batch, in seconds; must be greater than zero | 30 |
protected_paths applies to every task; a task's **Frozen** globs add to it, never replace it. A failed or timed-out gate exits 9, so CI can tell it apart from a failed task.
The run-wide set is not empty by default. Five root-anchored toolchain globs — .cargo/**, rust-toolchain, rust-toolchain.toml, .npmrc and pytest.ini — are unioned with whatever protected_paths lists, so they hold in a run you never configured. They close a verification hole rather than protect an artifact: a .cargo/config.toml carrying [target.<triple>] runner = "true", or a pytest.ini carrying addopts = -p no:terminal, makes every test a [shell:] criterion runs exit 0 without running. Freezing a file that doesn't exist is the point — it makes creating it a violation. The globs are root-anchored, so a pytest.ini or a .cargo/ in a subdirectory isn't matched, and manifests and lockfiles aren't among them, because adding a dependency is routine work. protected_path_defaults = false removes the five and leaves your own list in force on its own.
[verification]
protected_paths = ["docs/design/**"]
protected_path_defaults = true
completion_command = "cargo test && cargo clippy --all-targets -- -D warnings"
env_passthrough = ["DATABASE_URL"]
http_read_limit = 65536
standing_budget_seconds = 30
[[verification.standing]]
hint = "[no-grep: crate::state in shared/zurdo/src/criteria/**]"
why = "the criteria layer never reads run state"
[[verification.standing]]
hint = "[callers: provider::AgentCli::invoke <= 2]"
why = "one executor factory and one analyzer path spawn agents"
A body larger than http_read_limit is read not at all. A hint with a contains clause then fails with http_body_too_large, and its diagnostic names the key; a status-only hint is decided by its status alone. Raise the key when a PRD asserts contains against a page bigger than 64 KiB.
[[verification.standing]]
Repository-wide rules that must hold on every task, checked automatically on every iteration alongside the task's own acceptance criteria. Each entry is one rule:
| Field | Meaning |
|---|---|
hint | Exactly one automated hint, written in the full PRD syntax with brackets: [no-grep: ...], [file-absent: ...], [shell: ...], [symbol: ...], etc. Bare [manual] hints are refused at config load. |
why | Non-empty explanation of the rule; becomes the failure narrative when the hint goes red. |
A standing rule is a description of the tree as it is, applied automatically in both the local and clean-room verification passes. A rule that is red at pre-flight stops the run outright with exit code 3, naming the failing hint and its why, before any agent is invoked. An iteration where the standing set goes red after the task's criteria all passed costs an attempt, the same way a frozen-path violation does. The rule's why reaches the agent in the retry prompt as a # Standing Rules section only when it fails.
Standing rules use the same hint families, lints, and evidence recording as per-task criteria — they are parsed by the same parser, validated by the same lints, and fail with the same typed reasons. The standing set can be thought of as a set of criteria that applies to every task in the PRD, enforcing invariants that are too fundamental to trust to repetition.
[coverage]
The changed-line coverage gate: once a task's criteria pass, Zurdo measures
the share of its changed lines the tests ran, and fails the attempt under
an 80% threshold by default. It's an opt-out, not an opt-in: zurdo run
refuses to start on a config with no [coverage] table at all, so set
enabled = false for a complete opt-out that needs no source.
| Field | Meaning | Default |
|---|---|---|
enabled | Master switch; false is a complete opt-out, no source needed | true |
threshold | Required coverage percent (0–100) for a source with none of its own | 80 |
source is an array of [[coverage.source]] tables, one per
language/toolchain:
| Field | Meaning | Default |
|---|---|---|
name | Unique within [coverage] | required |
command | Coverage command, run at the repo root through sh -c | required |
format | lcov, go, or jacoco | required |
report | Report path the command writes | required |
include | Globs selecting this source's files (frozen-glob dialect) | required |
exclude | Globs excluded from include | [] |
threshold | Overrides [coverage].threshold for this source | unset |
probe | Shell command run once at pre-flight to confirm the tool is installed | unset |
hint | Remedy text printed when probe fails | unset |
source_roots | Root directories replacing this format's default path mapping | [] |
A failing probe exits 3, naming the source, the probe command, and
hint when one is set. A missing table, enabled = true with no source,
an unknown key or format, a negated glob, a threshold outside 0–100,
or two sources sharing a name all exit 2 before any agent runs. Only
zurdo run requires the table; report, review, validate and verify
accept a config without one.
How the gate judges a task:
- Only the sources whose
includematches a changed file run, and each one'sreportis deleted first, so a stale report is never measured. - A source fails under its threshold, when its command exits non-zero or
outlives
[timeouts].coverage_seconds, and when a changed file it matches is one its report never mentions. A Rust or Go file with no function body (only constants, types, or module declarations) is not counted, because neither tool records a line for it. A failed attempt counts against**Max-Attempts**, and the next prompt names the percent, the uncoveredfile:lineranges and the unreached files. - A task that changed no matching file, changed no executable line, or
passed at pre-flight is recorded
not-applicable. A task's**Coverage**key overrides the threshold or exempts the task. - A report that is missing after its command exited
0, does not parse, or names no file in the tree is a configuration error, not a failed attempt: the run stops with exit2. For a path-mapping error, setsource_roots. - The gate reads the config as it was when the run started, so an agent
editing
.zurdo/config.tomlmid-run cannot move the bar. A task that changes a build file (Cargo.toml,go.mod,package.json,pyproject.toml,pom.xml,build.gradle,.zurdo/config.toml, …) has it listed asbuild_filesfor the reviewer; build files are never frozen.
[coverage]
enabled = true
threshold = 80
[[coverage.source]]
name = "rust"
command = "cargo llvm-cov --lcov --output-path target/lcov.info"
format = "lcov"
report = "target/lcov.info"
include = ["**/*.rs"]
exclude = ["tests/**"]
probe = "cargo llvm-cov --version"
hint = "cargo install cargo-llvm-cov"
[skills]
Extra directories to search for the skills a PRD names, beyond the provider's own.
| Field | Meaning | Default |
|---|---|---|
search_paths | Directories checked at pre-flight | [] |
A missing skill is a warning, never an error. Zurdo never installs or edits skills a PRD names.
Skill prefixes. List bare skill names in **Skills**. Zurdo adds each provider's prefix when it renders the prompt:
| Provider | Invocation | Bundled-skill install path |
|---|---|---|
anthropic | /skill-name | .claude/skills/<name>/ |
codex | $skill-name | .agents/skills/<name>/ |
copilot | /skill-name | .agents/skills/<name>/ |
[skills]
search_paths = ["tools/skills", "~/team/skills"]
Indexes and agents
[lumen]
Turns on the structural index, the only gate for [symbol:], [references:] and [callers:] hints.
| Field | Meaning | Default |
|---|---|---|
enabled | Build the index and allow structural hints | false |
languages | rust, python, go, typescript, javascript | all five |
max_file_bytes | Larger files are skipped | 2097152 |
gc_grace_minutes | Older index generations become collectable | 10 |
[experimental] structural_hints is retired: it still parses, warns, and decides nothing.
[lumen]
enabled = true
languages = ["typescript", "javascript"]
[vela]
The optional watcher that keeps the Lumen index warm between runs.
| Field | Meaning | Default |
|---|---|---|
enabled | Auto-start the watcher after structural work | false |
idle_minutes | Exit after this long idle; 0 never | 30 |
debounce_ms | Window that merges bursts of file events | 200 |
[vela]
enabled = true
[pyxis]
Turns on the retrieval index behind zurdo pyxis and the MCP find_related tool.
| Field | Meaning | Default |
|---|---|---|
enabled | Build and query the index; needs [lumen] enabled | false |
Enabling it downloads nothing. zurdo pyxis install adds the optional embedding model.
[pyxis]
enabled = true
[mcp]
Lets zurdo run hand its own MCP server to the executor it launches.
| Field | Meaning | Default |
|---|---|---|
inject_server | Master switch; also needs [lumen] enabled | false |
strict_claude_config | Pass --strict-mcp-config to claude, hiding your other servers | false |
adapters.anthropic | Inject into claude (--mcp-config) | true |
adapters.codex | Inject into codex (-c mcp_servers.zurdo.*) | true |
adapters.copilot | Inject into copilot (--additional-mcp-config) | false |
Copilot is off because an organization's policy can block third-party MCP servers. See Agent access (MCP).
[mcp]
inject_server = true
[mcp.adapters]
copilot = false
[reason]
Stall diagnosis, post-mortems and the lesson library's limits.
| Field | Meaning | Default |
|---|---|---|
enabled | Call the reasoner on stalls and failures | false |
stall_attempts | Same-fingerprint attempts that make a stall (minimum 2) | 2 |
max_diagnoses_per_task | Reasoner calls per task | 2 |
max_reasoner_calls_per_run | Reasoner calls per run | 20 |
guidance_max_bytes | Cap on guidance carried into the next prompt | 4096 |
extract_lessons | Write a lesson on each recovery and accepted heal | true |
max_lessons_injected | Lessons per prompt; 0 turns injection off | 2 |
max_lessons | Library cap; the least-used go first | 200 |
Reading committed lessons needs no switch. enabled with no reasoner or analyzer role is a config-load error. See Diagnosis & lessons.
[reason]
enabled = true
Cost
The run banner shows the provider and its resolved effort map. Each iteration reports its model, tokens and estimated cost, so a wrong map shows up during the run.
[pricing.<model>]
Overrides the built-in price table, in USD per million tokens, for one model id.
| Field | Meaning | Default |
|---|---|---|
input | Input price (required) | built-in |
output | Output price (required) | built-in |
cache_write | Cache-write price | built-in |
cache_read | Cache-read price | built-in |
Prices match exact ids only, so a dated pin needs its own block or its cost shows as partial. Negative values fail config load.
[pricing.claude-sonnet-4-6]
input = 3.00
output = 15.00
cache_write = 3.75
cache_read = 0.30
[pricing.copilot]
Copilot bills premium requests, so its override is a request rate and per-model multipliers.
| Field | Meaning | Default |
|---|---|---|
request_rate | USD per premium request | built-in |
multipliers.<id> | Requests per call for that model | built-in |
multipliers.default | Covers auto and unmapped ids | 1.0 |
[pricing.copilot]
request_rate = 0.04
[pricing.copilot.multipliers]
default = 1.0
"claude-haiku-4.5" = 0.33