Skip to content

Configuration

Every service gets a sigil.toml at the repo root (or wherever you point --config). This page is the full reference.

[service]
name = "api"
[deploy]
compose_file = "docker-compose.yml"
health_url = "http://localhost:8080/health"
[judge]
provider = "ollama"
model = "qwen3:14b"
[service]
name = "api" # service identifier (required)
baseline = "merge-base" # merge-base | main | <ref>
[deploy]
compose_file = "docker-compose.yml"
health_url = "http://localhost:8080/health"
health_timeout_s = 60
env_file = ".env.test"

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 scenarios
service = "api" # …because `service` is set
KeyRequiredMeaning
pathyesDirectory, relative to the project root (the directory containing .sigil/).
servicenoWhen 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.

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.

See Configuring Judges.

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 = true
auto_merge = true
[policy]
max_staleness_for_allow_s = 60 # freshness gate
min_window_for_auto = 50 # min evals before AUTO promotion
min_clean_allow_rate = 0.98
cooldown_after_incident_h = 24

Satisfaction score thresholds, per priority:

[policy.thresholds]
p0 = { allow = 0.95, review = 0.85 } # below 0.85 -> BLOCK
p1 = { allow = 0.90, review = 0.75 }
p2 = { allow = 0.80, review = 0.60 }

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):

Terminal window
sigil run scenarios/ --env TEST_API_KEY=abc123 # explicit value
sigil run scenarios/ --env ALICE_PASSWORD # pass through from sigil's own
# environment — keeps secrets
# off the command line
[eval]
allowed_origins = ["http://127.0.0.1:9090"] # extra origins scenarios may reach

Endpoint 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.)