Return at least three distinct exit codes: success when nothing was found, a specific non-zero code when the scan ran and found issues above the threshold, and a different non-zero code when the tool itself failed. Without the split, a crash can look like a pass or a finding.
Why it matters
CI systems decide pass or fail from one number. If your tool returns zero for both a clean scan and a scan that could not reach its data source, the pipeline goes green on a scan that never happened. If it returns one for both findings and a crash, teams cannot tell a real issue from a broken tool, and they learn to ignore failures.
A simple scheme
| Code | Meaning | Pipeline should |
|---|---|---|
| 0 | Scan completed, nothing at or above the threshold | Pass |
| 1 | Scan completed, findings at or above the threshold | Fail, show findings |
| 2 | Tool error, bad arguments or unreadable input | Fail, show the error |
| 3 | Could not reach the service or data source | Fail or retry, by policy |
The exact numbers matter less than documenting them and keeping them stable. Changing a code in a minor release breaks every pipeline that depends on it.
Design rules
- Never exit zero on an incomplete scan. If only half the files were readable, say so and use an error code, or require an explicit flag to accept partial results.
- Make the threshold a flag. Users choose the severity level that fails the build, and the default should be written down.
- Print the reason, not just the code. A final line stating the code, the count of findings and the threshold makes logs easy to read.
- Support a report-only mode. Teams adopting a gate want to see results before enforcing. In that mode, return zero on findings but still non-zero on tool errors.
- Separate output formats from exit codes. A JSON report on disk should not change the exit status.
- Test the codes. Add tests that run your tool against a clean fixture, a vulnerable fixture, a missing file and a blocked network.
Think about timeouts
A hung network call that never returns is worse than a failure, because it holds a CI runner. Set a timeout, and return the service error code when it fires. Let users raise the limit with a flag.
Gates in the pull request
Exit codes are what let a pull request gate work. When a check returns a clear pass or fail, the platform can block a merge on it. The demo shows the idea from the developer's side.
Try it yourself
Open the demo and watch a pull request check go from pass to blocked. Notice that the reason is shown next to the status.
Click through it at your own pace, or open it full screen.
Quick check
Should a scan with findings and a scan that crashed use the same code?
No. Separate codes let teams respond correctly, and stop a crash being read as a pass.
Is it safe to change an exit code in a minor release?
Avoid it. Pipelines depend on the numbers. Treat changes as breaking and announce them.
Safeguard's policies and gates can block a pull request on defined conditions, and you can try a free scan at gold.safeguard.sh/scan.
Last reviewed: 19 October 2026.