Skip to content

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.

  • A run on this machine with a ledger. bravos resume lists 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.

With no policy file, bravos ci uses a built-in one:

Terminal window
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 breaches

Omit --run and it uses the latest run for the repository you are in.

.bravos/policy.toml
fail_on_severity = "critical"
fail_on = ["confirmed", "unverified"]
Terminal window
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_severity

Same 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

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:

.bravos/policy.toml
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_severity

max_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:

.bravos/policy.toml
fail_on_severity = "low"
max_confirmed = 2
fail_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 2

Three counted findings, four breaches: three severity breaches and the budget.

  1. 0, passed. The default policy above, on this run.

  2. 1, the policy was breached. Every FAIL above.

  3. 2, the gate could not evaluate.

    Terminal window
    bravos ci --run does-not-exist
    echo $?
    bravos: unknown run 'does-not-exist' (run_id='does-not-exist')
    2

    Distinguishing 2 from 0 is 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 2 as well, for the same reason. A finished run that found nothing still answers 0: the test is whether a ledger was written, not what is in it.

Terminal window
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.