Exit codes
postil review and postil doctor use three exit codes. A CI job that gates on the wrong signal (or catches everything as one bucket) either blocks merges it should not or lets through ones it should have stopped. This page is the precise contract.
| Code | Meaning | What CI should do |
|---|---|---|
0 | Clean review, or findings exist but all are below gate.failOn. | Pass the job. Nothing to act on. |
1 | At least one finding at or above gate.failOn (default error). | Fail the job. This is the signal branch protection should require; see the gate. |
2 | Operational error: bad arguments, a missing or invalid config, no API key, a diff/PR-metadata fetch that failed before a review could run, or any other unrecoverable setup problem. | Fail the job, but treat it as an infrastructure problem, not a code-quality verdict. Check the stderr message (postil: error: ...) rather than re-running blindly. |
0 vs 1: what the gate threshold means
Exit 0 does not mean zero findings: it means nothing crossed the gate threshold. A repo with gate.failOn: error can post several warn and info findings on postil/review and still exit 0, because postil/gate only cares about error-severity findings by default. Raise the threshold with --fail-on or gate.failOn: warn to make warnings block too. Preview the effect of a threshold change without spending model calls with postil plan.
1 always means the gate, never a crash
Exit 1 is a successful review that found something. The model ran, findings were produced and filtered, and at least one cleared the gate threshold. This is working as intended: the same exit code a linter uses for "found issues," not for "could not run."
2: what counts as operational
Exit 2 covers failures before or around the review itself, not failures in what the model said about the diff:
- Invalid CLI arguments or an unparseable config file.
- Missing required input: no
MODEL_API_KEY,POSTIL_API_KEY, orOPENROUTER_API_KEY; no--repoor--prfor remote review, no--staged/--base/--diff-filefor local review. - A pre-review forge call that failed outright: fetching PR metadata or the diff itself (auth failure, repository not found, network error) before any review content existed.
main.rs catches every error dispatch() returns, prints it as postil: error: <detail> to stderr, and exits 2. Nothing in Postil maps an operational error to exit 0; a failure to run a review is never reported as a pass.
The error-path advisory nuance
For remote reviews, a pre-review fetch failure both exits 2 at the process level and, on a best-effort basis, posts a synthetic error envelope to the forge's check-runs before the process exits, so the PR page reflects the failure even though the CLI invocation itself reports an operational error. That check-run outcome depends on gate.onError:
gate.onError: block(default):postil/gatecompletes as failing on the forge, matching the process exit code.gate.onError: advisory:postil/gatecompletes as passing on the forge (a provider outage should not freeze a merge queue), butpostil/reviewstill showsneutralwith the error, and the CLI process itself still exits2.gate.onErroronly changes what the forge check-run reports; it never changes the CLI exit code, and it never applies to unusable model output, only to provider-class failures (timeouts, outages, unreachable endpoints).
Once a review actually runs and the model returns output that cannot be validated, that is not an operational error: it is a gate-failing finding at .postil/model-output:1 (exit 1), because a malicious diff can otherwise induce unusable output through prompt injection and must never be rewarded with a silent pass. Fail closed applies uniformly regardless of gate.onError for that class.
postil doctor
postil doctor uses the same two-tier signal: 0 when every check passes, 1 when a check fails (its report format is documented in self-hosted). Exit 2 still applies to operational errors that happen before the checks can run: a config file that fails to parse exits 2 with a postil: error line, exactly like any other command.
Reference: what a CI job should check
postil review --repo owner/name --pr "$PR_NUMBER"
case $? in
0) echo "clean or below gate threshold" ;;
1) echo "gate-failing findings — block the merge" ; exit 1 ;;
2) echo "operational error — check postil: error output, not a code review verdict" ; exit 1 ;;
esacMost CI systems already treat a nonzero exit as job failure, so the case above is for jobs that want to log which branch fired before failing. See the CLI reference for every flag and the envelope schema for the JSON contract that --output-json prints alongside the exit code.