Skip to content

Exit codes

A CI job reads an exit code, not a summary line. This page is the whole mapping, per verb, then what a pipeline should do with each code, then the one distinction people get wrong: the run document's status is not the exit code.

Every command

These can end any command, run included.

Code Meaning
70 Internal error: an uncaught exception inside Veridor. The traceback goes to stderr, followed by veridor: internal error (<Type>). This is a bug in Veridor, not a finding about your target .... It is never a finding. It used to exit 1, which a pipeline reads as "the gate failed".
130 Stopped by SIGINT (Ctrl-C). Sequence teardown runs on the way out. stderr: veridor: stopped by SIGINT before the command finished (exit 130).
143 Stopped by SIGTERM: a cancelled CI job, a Kubernetes pod termination, a Job's activeDeadlineSeconds. It takes the same path as SIGINT, so teardown for the in-flight sequence runs first. A second SIGTERM during that teardown is ignored.

A run stopped by either signal before its probes finish writes no run document. SIGKILL cannot be caught: it ends the process without teardown, and fixtures a sequence created may be left behind.

How this was measured: the Veridor test suite's tests/test_process_exit.py (slow tier) starts a local server that records the teardown request, sends SIGINT or SIGTERM mid-sequence, and asserts the teardown DELETE arrived, the exit code, and that nothing was written to --output-dir; it also checks that an uncaught exception exits 70 and that python -m veridor propagates the code. Run for this page on the tree at commit 38025ed (macOS, 2026-11-09): 14 tests, OK.

veridor run

Code Meaning
0 Nothing the gate fails on was found, and coverage, if --openapi was passed, was measured. See what exit 0 means.
1 The gate failed on a finding.
2 Refused: configuration, credential, specification, path-safety or undefined-coverage error, or an output that could not be written. Includes a --read-only refusal, an unset or empty ${VAR}, an unknown key in a request block, a request path that would leave the target, a Host header, a proxy variable it cannot read or an https:// proxy for an https:// target, an input that is a file the run itself rewrites, and an unwritable output directory, each before any request.
3 No gating finding, but something the gate needs went unmeasured. See below.

2 is the only code that can mean nothing was sent, and it does not always mean that: boundary coverage is computed after the probes and never suppresses them, and a write that fails after the probes despite the check below (a disk that fills during the run, say) is reported as 2 after they were sent. For a --report file, that 2 does not replace a 1.

Before the first request, run, map, mutate and verify create each output directory and write and remove a test file in it, and in the directory of every report, and refuse with exit 2 if they cannot: veridor: cannot write the results of `run` to --output-dir ... Nothing has been sent, followed by the uid:gid and the fix. Without that check, a run pointed at a directory it cannot write sent every probe and then exited 2, and the finding it had measured was lost with it. Measured for this page on an earlier tree (before 38025ed), with a listener counting requests: the refusal (an --output-dir under a mode-555 directory) sent 0, and the same run into a writable directory sent 9. The full message is on the configuration reference.

What exit 0 means

Exit 0 means nothing the selected gate fails on was found. It does not mean every declared expectation held. Two things exit 0 as well:

  • A finding the policy does not gate on. Under the default --fail-on new+confirmed, a finding already in the baseline that is not graded confirmed; under --fail-on none, every finding.
  • An unanswered probe without --require-complete, as long as some probe was answered (a run in which not one was answered exits 3 with no flag).

The console summary and every report still list them. And an expectation that did hold may not have been able to fail: that is what mutate measures. Every shipped CI template's exit-0 line now says this. It used to say "Every declared expectation held", which the Veridor CHANGELOG records as printed over runs that recorded a vulnerable finding the baseline or the policy left ungated.

What produces 3

Condition Flag required Reason code
Not one probe was answered at all. none (any --fail-on but none) nothing_answered
An identity never authenticated, for a reason that is not transient. none (any --fail-on but none) identity_unauthenticated
A sequence teardown did not complete cleanly. --require-clean-teardown teardown_unclean
An expectation accepts both access and denial, so it asserts nothing. --require-complete expectations_non_asserting
Any probe went unanswered: a rate limit, a 5xx, a proxy's 407, a timeout, a body too large to scan. --require-complete inconclusive
A denial-only expectation did not establish who was denied: a credential Veridor fabricated drew the same denial, or none could be sent to compare with and nothing else proved the identity live. --require-complete liveness_unproven

The run document records which of these applied, in its gate object.

Answered means the authorization layer answered. A 429, a 5xx, a proxy's 407, a transport error and a body too large to scan are not answers. A target that returns 500 to everything exits 3, not 0.

The first two rows need no flag because the alternative is worse than a false alarm: a wrong base URL or an expired token would otherwise exit 0 and read as verified. Measured: the demo configuration pointed at a port with nothing listening exits 3 with the reason nothing_answered.

3 never replaces 1

A real finding always wins. The reasons are evaluated in precedence order, findings first, and the first one's code is returned. An unclean cleanup cannot mask, downgrade, or replace a security result.

--fail-on none is not "always zero"

none means the gate never fails on findings. It also stops the coverage error, nothing_answered, identity_unauthenticated and liveness_unproven from moving the code, so a report-only pipeline that passes --openapi does not start failing the day the specification drifts or a target goes down. The messages still print, and the run document lists each condition under gate.not_enforced.

It does not switch off --require-complete or --require-clean-teardown. Measured on an unreachable target: --fail-on none exits 0 (and the Markdown report opens with Gate exit 0, and nothing was measured), while --fail-on none --require-complete exits 3.

veridor verify on a capsule

Result Code
CONFIRMED 1
NOT_REPRODUCED 0
INCONCLUSIVE, or refused before sending 2

NOT_REPRODUCED needs a measurement behind it: every identity the check names was answered with a status the capsule declares, every marker the check grades was observed where it had to be, and the liveness control found no denial that a credential Veridor fabricated drew as well. Anything short of that is INCONCLUSIVE, and the reason names what was not measured: an unreachable target, a proxy answering 407, a VERIDOR_* token the target rejects, a target that refuses with a different status than the capsule declares (404 where it says 403). A maintainer whose instance was down, or whose token expired, must not be told the bug is not real. With --no-prove-liveness the control is off, and a NOT_REPRODUCED says so.

Measured for this page with a capsule written from a real finding: the fixed demo target gave NOT_REPRODUCED, exit 0; the vulnerable one CONFIRMED, exit 1; a listener answering 401 to everything, as a target does to stale tokens, INCONCLUSIVE, exit 2, naming both identities and the 401 each received. Before this rule a run whose probes drew an undeclared status fell through to NOT_REPRODUCED.

CONFIRMED needs less: an identity got an answer its declaration rules out. With suspected evidence (a status, no marker observed), anything that answers every request with 200 produces it, a proxy or a captive portal included; the Veridor README records a proxy answering 200 to everything drawing CONFIRMED, exit 1, while the target received none of the four requests. Before acting on a suspected CONFIRMED, look for the requests in the target's own access log. What a capsule run refuses is on safety posture.

veridor map

Code Meaning
0 It produced a map, whatever it found. Under --fail-on-drift none, whatever the comparison found.
1 You asked --fail-on-drift for a gate and the matrix moved against the baseline. A difference wins over 3.
2 It refused, or could not load its inputs: an over-budget plan, a missing target acknowledgement, an off-target path, --fail-on-drift without --baseline, a directory baseline under a gate, an output directory it cannot write.
3 The matrix records nothing (not one probed cell was measured), or, with --require-complete, some probed cell was not. Under --fail-on-drift opened or any, also a comparison in which not one cell was measured in both this run and the baseline. A redirect is not measured: Veridor never follows one.

Nothing map discovers can produce a non-zero code: only a diff against a baseline you recorded, or a grid it could not measure. A candidate is a hypothesis, and discovery must never fail somebody's build. Measured for this page on an earlier tree (before 38025ed): a baseline recorded while the target was down exited 3, and gating the vulnerable demo target against it exited 3 under opened (0 under none); the control, a baseline recorded against the fixed target, exited 1. Details on drift.

veridor mutate

Code Meaning
0 Every generated mutant was killed.
1 At least one mutant survived, so at least one check may be vacuous.
2 At least one mutant was aberrant or could not be evaluated, or none was generated at all, or the plan was refused before the first request (an output directory it cannot write included).
3 With --require-evaluable, some mutants could not be evaluated against this target.

A surviving mutant says your declaration is weak. It says nothing about your target. mutate's exit code is never a run's.

What a pipeline should do with each code

Code Treat it as Retry?
0 Pass: nothing the gate fails on was found. Not "every expectation held" (above), and not proof the boundary is enforced. —
1 Fail. A finding the policy gates on. No. A retried gate reports its luckiest attempt.
2 Fail. Configuration, credential or path problem; the veridor: line names it. Not a finding and not a pass. Only after fixing the cause.
3 Fail, unless you have deliberately chosen to show it as a warning. Something went unmeasured. Re-running may measure it, but read why first.
70 Fail, as a tool failure. Report the traceback. —
130, 143 Fail. Interrupted; nothing was concluded. Yes, the run did not finish.
anything else Fail. Not a Veridor code. —

This is what every shipped integration does. None of them uses || true, allow_failure: true or an exit 0 after the gate. Where a platform has a warning state, exit 3 can be shown as one, and only by an explicit opt-in:

Integration Exit 3 by default Opt-in warning
GitHub Action fails the job none; require-complete: false stops --require-complete from producing it
GitLab component fails the job allow_unmeasured: true → allow_failure: exit_codes: [3]
Jenkins FAILURE VERIDOR_ALLOW_UNMEASURED = 'true' in the Jenkinsfile → UNSTABLE
Azure Pipelines Failed VERIDOR_ALLOW_UNMEASURED: "true" in the YAML → SucceededWithIssues
AWS CodeBuild fails the build none: CodeBuild has no warning state
Argo CD PostSync, Helm hook, plain Job, Argo Rollouts Job Failed; sync or release Failed; rollout aborted none; gate.requireComplete: false in the chart

The run document's status is not the exit code

Every run document carries its own status. It has three values.

status Means
passed No finding at all: nothing vulnerable, unexpected_denial or inconclusive.
failed At least one finding is vulnerable or unexpected_denial.
inconclusive No finding is vulnerable or unexpected_denial, and at least one is inconclusive: a probe or sequence step that could not be graded. Other probes in the same run may still have been answered and matched.

status is computed from the findings alone. Conditions that produce no finding (a non-asserting expectation, unproven liveness, an unclean teardown) leave it passed; the gate object records them. A run in which not one probe was answered is inconclusive and carries the gate reason nothing_answered.

It is not the exit code, and it cannot be. The exit code depends on --fail-on and the other flags; the run's decision is recorded separately, in the gate object. The same failed document exits 0 under --fail-on none and 1 under --fail-on any.

status was two-valued and derived from the finding count until it was corrected. A run whose probes all timed out said failed while exiting 3, and the viewer rendered a red FAILED badge over a build CI had called green. A run that measured nothing is neither passed nor failed, so it now renders amber.

Read status and the exit code together

status: passed with exit 3 is what an unclean sequence teardown under --require-clean-teardown looks like: nothing was found about the target, and the target may still hold fixtures. status: inconclusive with exit 0 is a report-only run that measured nothing. Neither number alone tells you that; gate.reasons and gate.not_enforced do.

Related: reports, SARIF and code scanning, verdicts and evidence grades, what a finding does and does not mean.