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 gradedconfirmed; 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 exits3with 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.