Configuration
Every service gets a sigil.toml at the repo root (or wherever you point --config). This page is the full reference.
Minimal example
Section titled “Minimal example”[service]name = "api"
[deploy]compose_file = "docker-compose.yml"health_url = "http://localhost:8080/health"
[judge]provider = "ollama"model = "qwen3:14b"[service]
Section titled “[service]”[service]name = "api" # service identifier (required)baseline = "merge-base" # merge-base | main | <ref>[deploy]
Section titled “[deploy]”[deploy]compose_file = "docker-compose.yml"health_url = "http://localhost:8080/health"health_timeout_s = 60env_file = ".env.test"[[scenarios]]
Section titled “[[scenarios]]”Where scenario source lives. Optional — with no entry, sigil looks in
scenarios/ at the project root. Note the double brackets: this is an array of
tables, so a monorepo can declare several roots.
[[scenarios]]path = "scenarios" # holds one subdirectory per service
[[scenarios]]path = "services/api/scenarios" # IS the api service's scenariosservice = "api" # …because `service` is set| Key | Required | Meaning |
|---|---|---|
path | yes | Directory, relative to the project root (the directory containing .sigil/). |
service | no | When set, this directory is that service’s scenarios, with no service-name level. When absent, it holds one subdirectory per service. |
Discovery reads every root. Writes — sigil init, generation staging,
promotion — go to the first entry.
[keys]
Section titled “[keys]”age recipients for encrypted holdout scenarios. Every .lua.age holdout is
encrypted to every recipient listed here.
[keys]ci = "age1..."reviewer = "age1..."Manage with sigil keys add <name> <age1...> or sigil keys add-self; either
creates the table if absent. After adding a recipient, run sigil keys rotate
as a current key-holder to re-seal existing holdouts to it.
Promoting a scenario to holdout fails closed when no recipients are configured: an unencrypted holdout would be indistinguishable from a visible scenario.
[judge]
Section titled “[judge]”See Configuring Judges.
[generate]
Section titled “[generate]”Optional override for scenario generation (sigil scenario generate):
[generate]provider = "anthropic"model = "claude-opus-4-7"context_specs = ["docs/specs/auth.md", "docs/specs/billing.md"][ci]status_context = "sigil/api"comment = trueauto_merge = true[policy]
Section titled “[policy]”[policy]max_staleness_for_allow_s = 60 # freshness gatemin_window_for_auto = 50 # min evals before AUTO promotionmin_clean_allow_rate = 0.98cooldown_after_incident_h = 24[policy.thresholds]
Section titled “[policy.thresholds]”Satisfaction score thresholds, per priority:
[policy.thresholds]p0 = { allow = 0.95, review = 0.85 } # below 0.85 -> BLOCKp1 = { allow = 0.90, review = 0.75 }p2 = { allow = 0.80, review = 0.60 }Environment variables in scenarios
Section titled “Environment variables in scenarios”sigil.env("KEY") reads from a strict per-key allowlist — anything not named is
invisible to the scenario and returns nil.
The allowlist is populated by sigil run --env (repeatable, docker-style):
sigil run scenarios/ --env TEST_API_KEY=abc123 # explicit valuesigil run scenarios/ --env ALICE_PASSWORD # pass through from sigil's own # environment — keeps secrets # off the command line[eval]
Section titled “[eval]”[eval]allowed_origins = ["http://127.0.0.1:9090"] # extra origins scenarios may reachEndpoint pinning confines scenario HTTP to the deployed service’s origin by default — a holdout or contract scenario cannot exfiltrate over an arbitrary base_url. allowed_origins adds extra origins that sigil eval / sigil scenario run / sigil generate may reach (typically sidecars like a metrics endpoint on another port). Entries must be bare origins (scheme://host[:port], at most a trailing /); a malformed entry is a hard config error at load (fail-closed). The deployed service’s own origin is always allowed and need not be listed. (sigil run, which has no project config, pins to --endpoint and ignores this list; use its --allow-cross-origin flag instead.)