#Dynamic verification
Nyx re-runs findings in generated harnesses when verification is enabled. By
default, nyx scan verifies each Confidence >= Medium finding, tries
payloads in a sandbox, and writes the result to evidence.dynamic_verdict.
Default Nyx builds include the dynamic feature; custom
--no-default-features builds run static-only unless rebuilt with
--features dynamic.
Dynamic verification is a second signal, not a replacement for review. A
confirmed verdict means Nyx triggered the sink in its harness. NotConfirmed
means the harness ran but no payload fired.
#Running it
nyx scan # verifies Medium and High confidence findings
nyx scan --no-verify # static analysis only
nyx scan --verify # explicit form of the default behavior
Use --no-verify for fast local checks or editor workflows. Keep verification
on for CI when scan time allows it.
To verify low-confidence findings too:
nyx scan --verify-all-confidence
Use it when tuning payloads or investigating coverage. It is slower and noisier than the default.
#Verdicts
| Status | Meaning |
|---|---|
Confirmed |
At least one payload reached the expected sink in the harness. |
NotConfirmed |
The harness ran, but no payload reached the sink. Treat the original finding as still open until reviewed. |
Inconclusive |
Nyx could not finish the check with enough isolation or runtime support. |
Unsupported |
Nyx did not try the finding. Common causes are unsupported language, unsupported sink shape, missing flow steps, or confidence below the verification threshold. |
#Configuration
To disable verification for a project, set:
[scanner]
verify = false
This makes scans static-only unless the command line overrides it.
The related scanner settings are:
| Setting | Default | Meaning |
|---|---|---|
verify |
true |
Run dynamic verification after static analysis. |
verify_all_confidence |
false |
Include findings below Confidence::Medium. |
verify_backend |
"auto" |
Use Docker when available, otherwise use the process backend. |
harden_profile |
"standard" |
Hardening profile for the process backend. |
See Configuration for the full config table.
#Sandbox backends
nyx scan --backend docker # require Docker
nyx scan --backend process # run directly on the host with weaker isolation
nyx scan --unsafe-sandbox # alias for --backend process
Docker is the preferred backend. It mounts only the entry file's directory and blocks outbound network by default. Nyx binds a loopback OOB listener at scan start for callback-style payloads (SSRF, blind SSTI). When the bind succeeds, Docker switches to bridge networking with a host-gateway route so the harness can reach the listener; OOB payloads are skipped if the bind fails.
The process backend is useful for development and machines without Docker. It does not provide the same isolation.
#Repro artifacts
Confirmed findings write a repro bundle under:
~/.cache/nyx/dynamic/repro/<spec_hash>/
The bundle contains the harness spec, payload, expected output, trace, and
reproduce.sh.
cd ~/.cache/nyx/dynamic/repro/<spec_hash>
./reproduce.sh
./reproduce.sh --docker
Use the Docker form when the bundle records a pinned container image or when host toolchains differ from the original run.
#Runtime cost
Verification adds harness build time and sandbox startup time for each verified
finding. For quick local checks, --no-verify is usually the right choice. For
CI or scheduled scans, keep verification enabled so confirmed findings rank
higher and not-confirmed findings carry the extra context.
#Event log
Nyx writes verdict events to:
~/.cache/nyx/dynamic/events.jsonl
Each line is a JSON object with a versioned envelope:
{
"schema_version": 1,
"nyx_version": "0.7.0",
"corpus_version": "15",
"kind": "verdict",
"ts": "2026-05-15T18:42:09Z",
"finding_id": "a3b1...",
"spec_hash": "9f4e...",
"lang": "python",
"cap": "SQL_QUERY",
"status": "Confirmed",
"toolchain_id": "python-3.11",
"toolchain_match": "exact",
"duration_ms": 312,
"build_attempts": 1
}
The literal nyx_version and corpus_version values shift between releases; see crate::dynamic::telemetry::CORPUS_VERSION for the active payload-corpus version your binary writes.
| Field | Meaning |
|---|---|
schema_version |
Event schema version. Readers reject mismatches. |
nyx_version |
Version of the Nyx binary that wrote the event. |
corpus_version |
Payload corpus version used for the verdict. |
kind |
verdict or rank_delta. Feedback rows use an event: "verify_feedback" field instead and may pre-date the schema envelope. |
ts |
Write time in RFC 3339 format. |
finding_id |
Stable finding identifier. |
spec_hash |
Hash of the harness spec. |
lang |
Language slug, or unknown when spec derivation failed. |
cap |
Sink capability, such as SQL_QUERY or CODE_EXEC. |
status |
Confirmed, NotConfirmed, Inconclusive, or Unsupported. |
inconclusive_reason |
Present when status is Inconclusive. |
If the schema changes, move or delete the old events.jsonl before reading it
with the new binary. Programmatic readers should use
crate::dynamic::telemetry::read_events(path).
#Sampling
[telemetry] in nyx.toml controls event retention:
[telemetry]
keep_all_confirmed = true
keep_all_inconclusive = true
sample_rate_other = 1.0
sample_rate_other accepts 0.0 to 1.0 and applies to NotConfirmed and
Unsupported verdicts. The decision is deterministic for a given spec_hash.
Confirmed, Inconclusive, and rank-delta events are always kept by default.
Set NYX_NO_TELEMETRY=1 to disable event writes.
#Feedback
To record a bad verdict:
nyx verify-feedback <finding_id> --wrong "reason"
Feedback is written to the local event log. Nyx does not upload it.
#Browser UI
nyx serve shows dynamic verdicts on finding detail pages, uses them in
ranking, and can compare verdict changes between saved scans.
See Output formats for the dynamic_verdict schema.