#Configuration
Nyx Agent loads its configuration from nyx-agent.toml. The file is
optional: when missing, every section falls back to defaults, so
read-only commands like nyx-agent doctor work in a fresh checkout
with no config on disk.
#Where the file lives
The agent looks for ./nyx-agent.toml relative to the current
working directory. Override with --config <PATH> on any subcommand:
nyx-agent --config /etc/nyx-agent.toml serve
nyx-agent doctor prints whether the file was found and which
sections parsed.
#Schema overview
| Section | Purpose |
|---|---|
[general] |
Log level, state directory override. |
[performance] |
Static-pass concurrency and per-repo timeout. |
[sandbox] |
Sandbox enable + backend selection. |
[ai] |
AI runtime, model, concurrency, per-run budget cap. |
[ui] |
HTTP listen address, browser auto-open. |
[triggers] |
Push/PR triggers + webhook secret + branch filter. |
[nyx] |
Override the discovered nyx binary or its minimum version. |
[run] |
Verifier knobs and optional scanner orchestration. |
[env] |
Docker compose env-builder pull policy. |
[[project]] |
One block per product; declares repos. |
[project.launch] |
Optional local app orchestration for one project. |
[[schedule]] |
One cron-driven scan entry; repeated for multiple schedules. |
Unknown top-level fields and unknown fields inside any section are
rejected at parse time. A typo like state_directory = "..."
fails the load rather than silently going to default.
#[general]
| Field | Type | Default | Description |
|---|---|---|---|
log_level |
string | info |
tracing filter for stderr (e.g. info, debug, nyx=trace). |
state_dir |
path (optional) | unset | Override the state directory. Falls back to dirs::data_dir()/nyx-agent when unset. |
#[performance]
| Field | Type | Default | Description |
|---|---|---|---|
max_parallel_scans |
u32 | 4 |
Reserved for future use by the run dispatcher. |
scan_timeout_secs |
u64 | 600 |
Reserved for future use. |
static_concurrency |
usize (optional) | unset | Explicit fan-out for the static-pass. None lets the dispatcher compute min(num_cpus / 2, len(repos)). A configured 0 floors to 1. |
per_repo_timeout_secs |
u64 (optional) | unset | Per-repo budget for the static-pass scan. None resolves to 30 minutes. A repo that exceeds the budget records Inconclusive(StaticPassTimeout) while the rest of the run continues. |
scheduler_tick_secs |
u64 (optional) | unset | Cadence at which the cron scheduler wakes to evaluate [[schedule]] entries. None resolves to 60 seconds; a configured 0 floors to 1. Lower only when a sub-minute cron granularity is required; tighter polling spends more CPU. |
#[sandbox]
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Toggle for the sandbox layer. |
allow_network |
bool | false |
Whether sandboxed jobs may reach the network. |
backend |
enum | auto |
Backend selector. See below. |
backend values (kebab-case):
| Value | Lane / Platform |
|---|---|
auto |
Pick the strongest available backend at runtime. |
process |
No kernel isolation. Static-pass only. Always works. |
birdcage |
macOS Seatbelt profile shipped with the agent. |
libkrun |
Lightweight microVM on Linux via libkrun. |
firecracker |
Lightweight microVM on Linux via Firecracker. |
docker |
Docker container fallback. Slowest, requires the docker daemon. |
nyx-agent doctor prints which backends probe healthy on this host.
#[ai]
| Field | Type | Default | Description |
|---|---|---|---|
provider |
string (optional) | unset | Free-form provider tag surfaced in the UI. |
model |
string (optional) | unset | Model identifier (e.g. claude-opus-4-7). |
api_base |
string (optional) | unset | Base URL for local OpenAI-compatible runtimes. |
runtime |
enum | none |
AI runtime selection. See below. |
max_concurrent_one_shot |
u32 | 4 |
Cap on in-flight one_shot AI calls per run, shared across payload synthesis, spec derivation, and chain reasoning. 0 floors to 1 to avoid a deadlocked semaphore acquire. |
default_run_budget_usd_micros |
i64 (optional) | unset | Per-run AI budget cap in USD micros stamped on new (run_id, kind) rows. Unset means unlimited; set a positive value to enable the cap. |
payload_synthesis_per_call_cap_usd_micros |
i64 (optional) | unset | Per-call cap forwarded into each PayloadSynthesis Budget. Clamps a single call below the shared per-run bucket. Unset means unlimited unless the shared run cap is lower. |
spec_derivation_per_call_cap_usd_micros |
i64 (optional) | unset | Per-call cap for each SpecDerivation call. Same fall-back rules. |
chain_reasoning_per_call_cap_usd_micros |
i64 (optional) | unset | Per-call cap for the single ChainReasoning call. Same fall-back rules. |
novel_discovery_per_call_cap_usd_micros |
i64 (optional) | unset | Per-call cap for each NovelFindingDiscovery batch. Same fall-back rules. |
exploration_soft_cap_usd_micros |
i64 (optional) | unset | Per-task soft cap for AI Exploration. Crossing it emits a single warning; the run continues until the hard cap below trips. Falls back to $5.00 (5_000_000) when unset or non-positive. |
exploration_run_cap_usd_micros |
i64 (optional) | unset | Per-run hard cap for AI Exploration. The pass halts if cumulative spend reaches this value. Falls back to $10.00 (10_000_000) when unset or non-positive. |
runtime values (kebab-case):
| Value | Description |
|---|---|
none |
AI features off. Static-pass only. |
anthropic |
Hosted Anthropic API using the operator's API key. The wizard stores the key in the OS keychain under account ai-anthropic. |
local-llm |
Local OpenAI-compatible runtime (LM Studio, Ollama, vLLM). Endpoint goes in api_base; set model when the server requires one. Any bearer token lives in the keychain under ai-local-llm. |
claude-code |
Optional local adapter that drives an already-installed claude CLI on $PATH. |
codex |
Optional local adapter that drives an already-installed codex CLI on $PATH. |
API keys never live in TOML. The wizard at first launch writes them to the OS keychain.
Nyx Agent does not include or resell access to model providers. Direct API and local endpoint runtimes are the official BYOK paths; CLI runtimes are optional local adapters for users who have installed and authenticated those tools themselves under the relevant provider terms.
#[ui]
| Field | Type | Default | Description |
|---|---|---|---|
listen_addr |
string | 127.0.0.1:8765 |
host:port the HTTP + WebSocket server binds to. |
open_browser |
bool | true |
Open the SPA in the default browser on serve startup. --no-open / --headless overrides this at the CLI. |
The default address is loopback-only. Binding 0.0.0.0:...
exposes the agent without auth and is not recommended; pair with
TLS + token auth (see docs/security-posture.md once it lands).
#[triggers]
| Field | Type | Default | Description |
|---|---|---|---|
on_push |
bool | false |
Reserved trigger flag. |
on_pr |
bool | false |
Reserved trigger flag. |
schedule_cron |
string (optional) | unset | Reserved. Use [[schedule]] entries instead for current cron behavior. |
webhook_secret_ref |
string (optional) | unset | HMAC-SHA256 secret reference for POST /webhook/git. Shape: env:<NAME> resolves to the environment variable, anything else is treated as the literal secret. Empty secrets are rejected. When unset, the webhook handler returns 503. |
webhook_branch |
string (optional) | unset | Optional branch filter. When set, only payloads whose ref equals refs/heads/<branch> trigger a scan. |
See docs/triggers/webhook.md for the full handler contract.
#[nyx]
| Field | Type | Default | Description |
|---|---|---|---|
binary_path |
path (optional) | unset | Override the discovered nyx binary. When unset, the runner falls back to a $PATH lookup. |
min_version |
string (optional) | unset | Override the built-in minimum-supported nyx version. Useful in integration tests; the resolver clamps to the built-in floor (MINIMUM_NYX_VERSION) so an under-floor override does not silently weaken the check. |
#[run]
| Field | Type | Default | Description |
|---|---|---|---|
replay_stable_check |
bool | false |
When true, the deterministic payload runner re-executes each (vuln, benign) pair a second time and stamps replay_stable on the resulting VerifyResult. Adds roughly 2x cost per verify. |
allow_state_changing_live_probes |
bool | false |
Allow live verification plans to use HTTP methods likely to mutate target state (POST, PUT, PATCH, DELETE). |
browser_checks_enabled |
bool | false |
Allow browser-driven verification and auth-session acquisition when the local runtime is available. Confirmed browser attempts save redacted replay evidence under the run trace directory. |
exploit_mode_enabled |
bool | false |
Master opt-in for invasive verification. State-changing probes still require allow_state_changing_live_probes = true; setting that older flag by itself is not enough. |
exploit_dry_run |
bool | false |
Evaluate guarded live plans and write audit records without sending HTTP/browser traffic where feasible. |
business_logic_templates_enabled |
bool | true |
Generate first-class business-logic pentest candidates from route/auth metadata. Generated plans still pass through normal verifier safety gates. |
research_mode_enabled |
bool | false |
Enable Vuln Research Mode. This adds product-invariant hypotheses from the semantic route model and prior candidate memory, prioritizes those candidates in attack planning/exploration, and uses deeper research prompts. It does not relax live execution gates. |
unsafe_attack_agent_enabled |
bool | false |
Run the pre-MVP unrestricted local attack-agent phase after normal verification. Intended only for disposable user-owned dev apps; once invoked it does not use the guarded live-verifier policy. The phase runs seven specialist agents, then a critical chain hunter and final triage pass. |
business_logic_template_ids |
array of strings | [] |
Optional allowlist of template ids. Empty means every registered template is considered. Use nyx-agent business-logic templates or GET /api/v1/business-logic/templates to list ids. |
exploit_request_cap |
int (optional) | unset (10) |
Per-candidate cap for guarded live HTTP/browser actions. 0 is floored to 1. |
exploit_requests_per_second |
int (optional) | unset (5) |
Per-candidate rate limit for guarded live HTTP/browser actions. 0 is floored to 1. |
exploit_reset_after_state_changing |
bool | true |
Ask the environment orchestration layer to reset/rollback after allowed state-changing probes when the active environment supports it. |
enable_zap_baseline |
bool | true |
Run zap-baseline.py against live target URLs when the binary is present on PATH. Missing binaries are skipped. |
enable_nuclei |
bool | true |
Run nuclei against live target URLs when the binary is present on PATH. Missing binaries are skipped. |
enable_trivy |
bool | true |
Run trivy fs against repo workspaces when present on PATH. Dependency, IaC, and secret findings become AI exploration context. |
enable_osv_scanner |
bool | true |
Run osv-scanner against repo workspaces when present on PATH. Dependency findings become AI exploration context. |
enable_secret_scanning |
bool | true |
Run gitleaks against repo workspaces when present on PATH; if absent, fall back to detect-secrets. Secret findings become AI exploration context. |
enable_katana |
bool | true |
Run katana against live target URLs when present on PATH. Sensitive crawled routes become live-test candidates. |
enable_httpx |
bool | true |
Run ProjectDiscovery httpx against live target URLs when present on PATH. Interesting HTTP metadata becomes live-test candidates. |
enable_aggressive_sqlmap |
bool | false |
Reserved for explicit sqlmap use. sqlmap is not auto-enabled because it is aggressive and has GPL/proprietary-integration constraints. |
Exploit mode is intentionally a two-key system. Nyx Agent defaults to
non-destructive live verification. State-changing HTTP methods,
browser actions that may mutate data, and aggressive external probes
are rejected unless exploit mode is enabled and the specific
state-changing gate is also enabled. The local server UI exposes the
same per-run controls in the Start pentest modal, so an operator
can opt into an invasive run without editing nyx-agent.toml.
Example opt-in for a disposable local target:
[run]
exploit_mode_enabled = true
allow_state_changing_live_probes = true
exploit_request_cap = 5
exploit_requests_per_second = 2
exploit_reset_after_state_changing = true
business_logic_template_ids = ["tenant_object_isolation", "file_permission_revalidation"]
Use exploit_dry_run = true to inspect generated policy audit
records before sending live traffic. With both safety keys enabled,
dry-run still generates selected business-logic candidates and records
template summary rows, but the live verifier does not send guarded
HTTP/browser traffic.
Business-logic templates that seed objects, submit coupon/price data,
change permissions, deliver webhook payloads, or send chatbot prompts
are only generated when the same two gates are enabled. They still
pass through request caps, rate limits, target URL scope checks,
auth-session acquisition, and reset-after-state-changing handling.
See business-logic-templates.md for
template ids, dry-run examples, skip reasons, and provenance shape.
Vuln Research Mode is separate from exploit mode:
[run]
research_mode_enabled = true
Research mode increases reasoning depth and candidate generation for
authorized product-logic review. It adds ResearchMode candidates for
invariants such as lifecycle bugs, stale access, replay, downgrade or
entitlement mismatch, invite/team/org transitions, webhook/event
consistency, AI-agent indirect actions, and background job side
effects. The candidates carry research_mode_provenance in
affected_components, and research-mode exploration findings carry the
same provenance in their verdict blob. Live HTTP/browser execution
still goes through the same target scope, request cap, rate limit,
exploit-mode, state-changing, dry-run, and reset gates.
Unsafe attack-agent mode is a separate pre-MVP local-only phase:
[run]
unsafe_attack_agent_enabled = true
It runs after normal verification while the configured local app is
still up. The agent receives repo workspaces, target URLs, prior
candidates, and existing vulnerabilities, then may use CLI tools to
attack the dev app directly and record verified_vulnerabilities with
proof artifacts. It is intentionally not routed through
ExploitSafetyPolicy; use it only against disposable local targets.
When enabled, the phase runs serially as nine attack passes:
| Pass | Focus |
|---|---|
business_logic |
Workflow, state-machine, role-transition, invite, quota, entitlement, lifecycle, replay, and order-of-operation bugs. |
payments_billing |
Checkout, subscriptions, invoices, coupons, trials, refunds, webhooks, payment status, and plan enforcement. |
user_data_privacy |
IDORs, cross-tenant reads/writes, exports/imports, files, logs, analytics payloads, admin views, and deleted-user data. |
auth_session |
Authentication, authorization, sessions, cookies, password reset, magic links, OAuth, MFA, CSRF, account linking, and privilege escalation. |
api_input |
Mass assignment, validation gaps, schema mismatch, hidden fields, file uploads, SSRF-like fetches, parser confusion, injection, and deserialization. |
infra_dev_prod |
Secrets, env config, debug routes, local services, dev mailers, seed credentials, logs, queues, storage, admin tooling, CORS, and deployment assumptions. |
abuse_automation |
Rate limits, brute force, enumeration, scraping, invite/email/SMS spam, cost abuse, queue flooding, resource exhaustion, and free-tier bypass. |
critical_chain_hunter |
Cross-domain exploit chains that combine smaller primitives into account takeover, cross-tenant compromise, payment bypass, persistent admin access, or secret exposure. |
triage |
Deduplication, dev-only classification, focused confirmation, and material severity/impact upgrades. |
Each pass receives the current candidates and verified vulnerabilities, including findings produced by earlier attack passes. The prompt also warns that dev mailers, mock payment providers, localhost-only services, seed credentials, debug routes, and synthetic fixtures are not production vulnerabilities by themselves unless the same trust boundary is production-relevant or the behavior creates a real local risk. Each attack pass gets a 30-minute agent-loop wall-clock cap, so an individual specialist is not stopped by the generic 15-minute adapter default.
Optional scanner findings are persisted as pentest candidates. Live web findings still pass live verification before surfacing as verified vulnerabilities; source/package findings are recorded as bounded context for AI exploration and triage. Nyx Agent does not bundle these scanners; it invokes local executables on PATH when available.
| Tool | Upstream license | Commercial use note |
|---|---|---|
OWASP ZAP / zap-baseline.py |
Apache-2.0 | Generally compatible with commercial products when notices and other Apache-2.0 conditions are preserved. |
| Nuclei | MIT | Generally compatible with commercial products when the MIT copyright and permission notice are preserved. |
| Trivy | Apache-2.0 | Generally compatible with commercial products when notices and other Apache-2.0 conditions are preserved. |
| OSV-Scanner | Apache-2.0 | Generally compatible with commercial products when notices and other Apache-2.0 conditions are preserved. |
| Gitleaks | MIT | Generally compatible with commercial products when the MIT copyright and permission notice are preserved. |
| detect-secrets | Apache-2.0 | Generally compatible with commercial products when notices and other Apache-2.0 conditions are preserved. |
| Katana | MIT | Generally compatible with commercial products when the MIT copyright and permission notice is preserved. |
| ProjectDiscovery httpx | MIT | Generally compatible with commercial products when the MIT copyright and permission notice is preserved. |
| sqlmap | GPL-2.0-or-later with project clarifications / optional commercial license | Internal use is usually different from redistribution, but embedding or parsing sqlmap results in proprietary software is treated by sqlmap upstream as requiring GPL compliance or a separate sqlmap commercial license. Get legal review before shipping this path. |
#[[project]]
Each [[project]] block declares one product and groups its repos
under nested [[project.repo]] blocks. Top-level [[repo]] blocks
are rejected by the parser.
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | n/a | Unique project name. Also used as the workspace directory prefix. |
description |
string (optional) | unset | Free-form description surfaced in the UI. |
target_base_url |
string (optional) | unset | Base URL the sandbox env-builder dials for dynamic checks against the running stack. |
env_config |
TOML value (optional) | unset | Structured env overrides merged into the project's docker-compose / sandbox runtime. Opaque to the agent. |
launch |
table (optional) | unset | Build/start/health/seed/reset/login recipe used before scans. See below. |
runtime_profile |
table (optional) | unset | Auth/session metadata for live verification. Prefer env refs over raw secrets. |
repo |
[[project.repo]] |
empty | Repos belonging to this project. See below. |
When [project.launch] is omitted, scans still run. If a project has
target_base_url but no stored launch profile, Nyx Agent creates a
conservative already-running profile and health-checks the target URL.
For local-path repos, mode = "auto" can detect root-level Docker
Compose files or simple package.json / Cargo.toml start commands.
#[project.launch]
| Field | Type | Default | Description |
|---|---|---|---|
name |
string (optional) | local dev |
Operator label for the default launch profile. |
mode |
string (optional) | inferred | auto, already-running, custom-commands, or docker-compose. |
target_urls |
array of strings | [target_base_url] when set |
In-scope local URLs handed to live probes and AI exploration. |
env_files |
array of strings | [] |
Env files resolved relative to the command working directory. |
env_vars |
array of tables | [] |
Env var names forwarded from the daemon process. |
build / start / seed / login / reset / stop |
array of command tables | [] |
Ordered shell commands for the target lifecycle. |
health |
array of tables | target URL check | Readiness checks. HTTP checks retry until timeout; command checks must exit 0. |
Command table fields:
command = "npm run dev"
repo = "web" # alias: repo_name
working_directory = "apps/web"
timeout_secs = 120 # alias: timeout_seconds
stdin = "y\n" # optional text written to stdin after spawn
HTTP health check:
[[project.launch.health]]
url = "http://localhost:3000/health"
timeout_secs = 60
Seed commands run after the app is healthy and before the static and
live scan phases. Login commands run after seed hooks and are intended
for local session setup. Reset commands run after state-changing live
probes when [run] exploit_reset_after_state_changing = true. Start,
build, seed, login, reset, and stop stdout/stderr are captured under
<state>/logs/environment/<run-id>/.
#[project.runtime_profile]
Runtime profiles describe authenticated roles that live verification can use. Each role gets a named auth session, and authorization probes can compare the same request as different roles.
[project.runtime_profile]
target_base_url = "http://localhost:3000"
[[project.runtime_profile.auth_profiles]]
role = "user_a"
mode = "header_injection"
bearer_token_env = "NYX_AGENT_USER_A_TOKEN"
tenant = "tenant-a"
[[project.runtime_profile.auth_profiles.owned_objects]]
name = "project"
id = "proj-user-a-1"
route = "/api/projects/{id}"
marker = "nyx-agent-user-a-project"
[[project.runtime_profile.auth_profiles]]
role = "user_b"
mode = "header_injection"
bearer_token_env = "NYX_AGENT_USER_B_TOKEN"
tenant = "tenant-b"
[[project.runtime_profile.auth_profiles]]
role = "admin"
mode = "session_import"
session_import_path = "sessions/admin-storage-state.json"
owned_objects are optional pre-seeded IDs for horizontal
authorization checks. Nyx Agent treats the id and marker as positive live
evidence markers: user_b must receive the same marker from user_a's
object before an IDOR-style vulnerability can verify.
tenant is optional metadata used in the Authorization Matrix. When a
role comparison or object ownership check runs, Nyx Agent records one row
for the allowed control and one row for the challenged access with the
role, tenant, resource/object, owner role, action, endpoint, expected
decision, observed HTTP/marker result, confidence, candidate id,
verification attempt id, and run id.
The local UI exposes an AI setup action in the auth profiles panel for
projects with local repos. Clicking it inspects the checked-out source
for login routes, object-shaped routes, and admin signals, then saves
named role profiles and any seeded owned objects into
project.runtime_profile. Pentests do not run this discovery step; they
only use the saved profiles selected before the run.
#[[project.repo]]
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | n/a | Unique repo name. |
i_own_this |
bool | false |
Operator attestation. The daemon refuses to ingest a repo without i_own_this = true. |
enabled |
bool | true |
Skip the repo when set to false. |
source |
table | n/a | Repo source. Either a git or local-path variant (tag field kind). |
source variants:
source = { kind = "git", url = "[email protected]:org/repo.git", branch = "main", auth = "ssh-key:~/.ssh/id_ed25519" }
source = { kind = "local-path", path = "/srv/repos/monolith" }
auth accepted shapes: ssh-key:<path>, token-env:<var>,
gh-app:<id>. Unknown kind values fail the load.
#[[schedule]]
One cron-driven scan entry. The scheduler evaluates every entry once per minute and fires matching ones against the configured repo filter.
| Field | Type | Default | Description |
|---|---|---|---|
cron |
string | n/a | 5-field cron expression (minute hour day-of-month month day-of-week). Example: 0 3 * * 1 = 03:00 every Monday. |
repo |
string (optional) | unset | Limit the run to one configured repo. None scans every enabled repo. |
label |
string | "scheduled" |
Operator-readable label surfaced in tracing spans and the UI. |
See docs/triggers/cron.md for the full scheduler contract.
#Worked example
[general]
log_level = "info"
[performance]
static_concurrency = 4
per_repo_timeout_secs = 1200
[sandbox]
enabled = true
allow_network = false
backend = "auto"
[ai]
runtime = "anthropic"
model = "claude-opus-4-7"
max_concurrent_one_shot = 4
default_run_budget_usd_micros = 2_500_000 # Optional: $2.50 per run
[ui]
listen_addr = "127.0.0.1:8765"
open_browser = true
[triggers]
webhook_secret_ref = "env:NYX_WEBHOOK_SECRET"
webhook_branch = "main"
[nyx]
# binary_path = "/opt/nyx/bin/nyx"
[run]
replay_stable_check = false
business_logic_templates_enabled = true
business_logic_template_ids = []
[[project]]
name = "acme-app"
description = "Acme web product"
target_base_url = "http://localhost:3000"
[project.launch]
mode = "custom-commands"
env_files = [".env.test"]
[[project.launch.build]]
command = "npm ci"
repo = "acme-frontend"
[[project.launch.start]]
command = "npm run dev"
repo = "acme-frontend"
timeout_secs = 120
[[project.launch.health]]
url = "http://localhost:3000/health"
timeout_secs = 60
[[project.launch.seed]]
command = "npm run seed:test"
repo = "acme-backend"
[[project.launch.reset]]
command = "npm run db:reset"
repo = "acme-backend"
[[project.repo]]
name = "acme-backend"
i_own_this = true
source = { kind = "local-path", path = "/srv/repos/acme-backend" }
[[project.repo]]
name = "acme-frontend"
i_own_this = true
source = { kind = "git", url = "[email protected]:acme/frontend.git", branch = "main" }
[[schedule]]
cron = "0 3 * * 1"
repo = "acme-backend"
label = "weekly-monday-3am"
#Failure modes
| Symptom | Cause / Fix |
|---|---|
failed to parse config at <path>: unknown field ... |
Typo in a field name. Sections deny unknown fields. Check the schema tables above. |
failed to parse config at <path>: missing field ... |
A required field is unset. name on [[project]] / [[project.repo]], cron on [[schedule]], url on a git source. |
failed to read config at <path>: ... |
Permission issue or the path passed to --config is wrong. |
no repositories selected; configure one in nyx-agent.toml |
The TOML has zero [[project.repo]] blocks or every repo is enabled = false. |
invalid [[schedule]] config: ... |
A cron expression failed to parse or the referenced repo does not exist. The daemon refuses to start. |
Daemon accepts webhooks but every call returns 503 |
triggers.webhook_secret_ref is unset, points at an unset env var, or resolves to an empty string. |