Gate a build on a run
bravos ci reads a run’s ledger, applies a policy, and exits with a code your
pipeline can branch on. It contacts no server, needs no credential, and is free.
This page writes a policy, sees it pass and fail, and spends most of its time on the thing that decides whether the gate means anything: which verdicts it counts.
Before you start
Section titled “Before you start”- A run on this machine with a ledger.
bravos resumelists what is available.
The transcripts below come from the small annotated repository in Build your first threat model: four threats tracked, three of them unverified criticals, one suppressed. Nothing in it has been probed.
The default policy
Section titled “The default policy”With no policy file, bravos ci uses a built-in one:
bravos ci PASS 20260910-004305-demo policy built-in default fail_on_severity high · max_confirmed none · fail_on confirmed
4 threats tracked · 0 counted · 0 breachesOmit --run and it uses the latest run for the repository you are in.
Write your own
Section titled “Write your own”fail_on_severity = "critical"fail_on = ["confirmed", "unverified"]bravos ci FAIL 20260910-004305-demo policy /Users/you/demo/.bravos/policy.toml fail_on_severity critical · max_confirmed none · fail_on confirmed, unverified
4 threats tracked · 3 counted · 3 breaches
critical #orders-dao #sqli app/data/orders-dao.js:8 breached fail_on_severity critical #orders-dao #sqli app/data/orders-dao.js:4 breached fail_on_severity critical #orders #idor app/routes/orders.js:7 breached fail_on_severitySame run, same ledger, opposite answer. One key changed.
bravos ci reads .bravos/policy.toml from the repository by default, so
committing it there is enough. --policy points at a different one.
| Key | Default | Effect |
|---|---|---|
fail_on_severity |
high |
a counted finding at or above this severity fails the build |
max_confirmed |
none | budget for counted findings before the build fails |
fail_on |
["confirmed"] |
which verdicts count at all |
fail_on is the one to think about
Section titled “fail_on is the one to think about”It takes ledger verdicts, so a team
can decide what a build is allowed to ignore. Counting error makes a probe
that never reached the target break the build instead of passing quietly.
Counting suppressed makes a @mitigates that removed an exposure from the
export break it:
fail_on_severity = "high"fail_on = ["suppressed"] FAIL 20260910-004305-demo policy /Users/you/demo/.bravos/policy.toml fail_on_severity high · max_confirmed none · fail_on suppressed
4 threats tracked · 1 counted · 1 breach
high orders xss app/routes/orders.js:13 breached fail_on_severitymax_confirmed is a budget on whatever is counted
Section titled “max_confirmed is a budget on whatever is counted”Its name says confirmed, and it counts whatever fail_on counts. It is
reported as its own breach, separately from the severity ones:
fail_on_severity = "low"max_confirmed = 2fail_on = ["unverified"] FAIL 20260910-004305-demo policy /Users/you/demo/.bravos/policy.toml fail_on_severity low · max_confirmed 2 · fail_on unverified
4 threats tracked · 3 counted · 4 breaches
critical #orders-dao #sqli app/data/orders-dao.js:8 breached fail_on_severity critical #orders-dao #sqli app/data/orders-dao.js:4 breached fail_on_severity critical #orders #idor app/routes/orders.js:7 breached fail_on_severity max_confirmed 3 unverified findings exceed the budget of 2Three counted findings, four breaches: three severity breaches and the budget.
The three exit codes
Section titled “The three exit codes”-
0, passed. The default policy above, on this run.
-
1, the policy was breached. Every
FAILabove. -
2, the gate could not evaluate.
Terminal window bravos ci --run does-not-existecho $?bravos: unknown run 'does-not-exist' (run_id='does-not-exist')2Distinguishing
2from0is the point. A gate that cannot find its ledger has proven nothing, and a pipeline that treats “could not evaluate” as “passed” is a green light with nothing behind it.A run that started and died before writing a ledger answers
2as well, for the same reason. A finished run that found nothing still answers0: the test is whether a ledger was written, not what is in it.
Machine-readable output
Section titled “Machine-readable output”bravos ci --format json{ "schema": "bravos.ci/v1", "run_id": "20260910-004305-demo", "codebase": "/Users/you/demo", "policy": { "source": null, "fail_on_severity": "high", "max_confirmed": null, "fail_on": [ "confirmed" ] }, "passed": true, "exit_code": 0, "counts": { "total": 4, "counted": 0, "by_severity": {}, "breaches": 0 }, "breaches": [], "findings": []}"source": null is the built-in policy; with a file it names the path.
exit_code is in the payload as well as on the process, so a step that captured
stdout does not have to have kept $?. On a failing run, breaches carries one
object per breach and findings the findings behind them.
Related
Section titled “Related”- Run Bravos in CI/CD: this gate wired into a pipeline, alongside the push.
- Read a run: the ledger this gate reads.
- Verdicts and the ledger: what
fail_onis choosing between.

