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

  1. CLI flag. Wins over everything, for example --max-iterations.
  2. PRD task metadata. Max-Attempts and Agent-timeout override the config for that task.
  3. 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.

ProviderCLISign in
anthropicclaude (Claude Code)claude login or ANTHROPIC_API_KEY
codexcodex (Codex CLI)codex login or OPENAI_API_KEY
copilotcopilot (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.

FieldMeaningDefault
provideranthropic, codex or copilotanthropic

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.

FieldMeaningDefault
providerAny configured provideranthropic
modelOne model id; the effort map doesn't applyclaude-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.

FieldMeaningDefault
providerAny configured providerfalls back to [roles.analyzer]
modelOne model idfalls 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.

FieldMeaningDefault (anthropic)
lowModel for **Effort**: lowclaude-haiku-4-5
mediumModel for **Effort**: mediumclaude-sonnet-4-6
highModel for **Effort**: highclaude-opus-4-7
any other keyLabels 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.

FieldMeaningDefault
cliBinary on PATH, or a wrapper scriptclaude, codex, copilot
extra_argsAppended 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.

FieldMeaningDefault
max_attemptsAttempts per task when the PRD omits **Max-Attempts**3
max_total_iterationsAgent calls across the run; 0 is unlimited0
parallel_criteriaCheck a task's criteria concurrentlyfalse
analyzer_parallelismConcurrent 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.

FieldMeaningDefault
criterion_secondsPer shell: or http: hint; a hint's timeout: overrides it300
agent_secondsPer agent call when the task omits **Agent-timeout**1800
completion_secondsThe completion gate; not seeded by init900
coverage_secondsEach coverage source's command, in the coverage gate below900
[timeouts]
criterion_seconds = 300
agent_seconds     = 1800
coverage_seconds  = 900

[verification]

Run-wide frozen paths, prompt priming, and the run-end completion gate.

FieldMeaningDefault
protected_pathsGlobs no task may change; a change fails the iteration[]
protected_path_defaultsUnion the five toolchain defaults below into the run-wide settrue
prime_contextAdd an # Evidence Paths section to the executor prompttrue
completion_commandOne shell command run once after every task passesunset
env_passthroughExtra environment variables a [shell:] criterion and completion_command may see, beyond the fixed allowlist[]
http_read_limitHow many bytes of an [http:] response body the verifier reads; 0 is rejected65536
standingStanding rules checked on every task, written as [[verification.standing]] entries[]
standing_budget_secondsTime limit for the standing-rule batch, in seconds; must be greater than zero30

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.

The completion gate: it runs only when every task passed, exiting 0 on success, 9 on failure and 5 if skippedThe completion gate: it runs only when every task passed, exiting 0 on success, 9 on failure and 5 if skipped
[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:

FieldMeaning
hintExactly 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.
whyNon-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.

FieldMeaningDefault
enabledMaster switch; false is a complete opt-out, no source neededtrue
thresholdRequired coverage percent (0–100) for a source with none of its own80

source is an array of [[coverage.source]] tables, one per language/toolchain:

FieldMeaningDefault
nameUnique within [coverage]required
commandCoverage command, run at the repo root through sh -crequired
formatlcov, go, or jacocorequired
reportReport path the command writesrequired
includeGlobs selecting this source's files (frozen-glob dialect)required
excludeGlobs excluded from include[]
thresholdOverrides [coverage].threshold for this sourceunset
probeShell command run once at pre-flight to confirm the tool is installedunset
hintRemedy text printed when probe failsunset
source_rootsRoot 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 include matches a changed file run, and each one's report is 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 uncovered file:line ranges 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 exit 2. For a path-mapping error, set source_roots.
  • The gate reads the config as it was when the run started, so an agent editing .zurdo/config.toml mid-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 as build_files for 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.

FieldMeaningDefault
search_pathsDirectories 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:

ProviderInvocationBundled-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.

FieldMeaningDefault
enabledBuild the index and allow structural hintsfalse
languagesrust, python, go, typescript, javascriptall five
max_file_bytesLarger files are skipped2097152
gc_grace_minutesOlder index generations become collectable10

[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.

FieldMeaningDefault
enabledAuto-start the watcher after structural workfalse
idle_minutesExit after this long idle; 0 never30
debounce_msWindow that merges bursts of file events200
[vela]
enabled = true

[pyxis]

Turns on the retrieval index behind zurdo pyxis and the MCP find_related tool.

FieldMeaningDefault
enabledBuild and query the index; needs [lumen] enabledfalse

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.

FieldMeaningDefault
inject_serverMaster switch; also needs [lumen] enabledfalse
strict_claude_configPass --strict-mcp-config to claude, hiding your other serversfalse
adapters.anthropicInject into claude (--mcp-config)true
adapters.codexInject into codex (-c mcp_servers.zurdo.*)true
adapters.copilotInject 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.

FieldMeaningDefault
enabledCall the reasoner on stalls and failuresfalse
stall_attemptsSame-fingerprint attempts that make a stall (minimum 2)2
max_diagnoses_per_taskReasoner calls per task2
max_reasoner_calls_per_runReasoner calls per run20
guidance_max_bytesCap on guidance carried into the next prompt4096
extract_lessonsWrite a lesson on each recovery and accepted healtrue
max_lessons_injectedLessons per prompt; 0 turns injection off2
max_lessonsLibrary cap; the least-used go first200

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.

FieldMeaningDefault
inputInput price (required)built-in
outputOutput price (required)built-in
cache_writeCache-write pricebuilt-in
cache_readCache-read pricebuilt-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.

FieldMeaningDefault
request_rateUSD per premium requestbuilt-in
multipliers.<id>Requests per call for that modelbuilt-in
multipliers.defaultCovers auto and unmapped ids1.0
[pricing.copilot]
request_rate = 0.04

[pricing.copilot.multipliers]
default            = 1.0
"claude-haiku-4.5" = 0.33