Run Bravos in CI/CD
There are two different things people mean by “Bravos in CI”, and only one of them needs a licence.
| What it is | Needs a licence | |
|---|---|---|
| Gate and file | a pipeline reads a ledger a run already produced, fails the build on it, and files it with a server | no (the push needs a server, which is licensed) |
| Run the loop | the pipeline itself annotates, probes and verifies, inside the prebuilt runner image | yes |
This page covers the first in full, because it works today with nothing but the free CLI. The second is described at the end, honestly, including what is not published yet.
Before you start
Section titled “Before you start”- The CLI on the runner. See Install Bravos.
- A run whose ledger is available to the job. On a pipeline that does not run
the loop itself, that means the
$BRAVOS_HOMEthe job can see holds one. - For the push step: a server URL and a
membertoken. See Connect the CLI to a server.
The two steps
Section titled “The two steps”bravos ci and bravos push are different commands and only one of them talks
to a server. A pipeline usually runs both: the gate decides whether the build
passes, and the push makes the finding visible to the security team.
- run: bravos ci # the gate. local, no server, no credential- run: bravos push # files the run with bugb-server env: BRAVOS_SERVER_URL: https://bravos.example.com BRAVOS_TOKEN: ${{ secrets.BRAVOS_TOKEN }}bravos ci |
bravos push |
|
|---|---|---|
| Contacts a server | no | yes |
| Credential | none | BRAVOS_TOKEN, role member or above |
| Where the credential lives | n/a | your CI provider’s secret store |
| Decides the build | yes, by exit code | no |
| Runs a scan | no | no |
Neither command runs a scan. Both read an artifact a completed run already wrote, which is why either can be re-run as often as the pipeline likes and always answers the same way.
-
Order them gate-first.
bravos pushfails on a server that is unreachable or whose licence has lapsed, and a push placed before the gate would fail the build for a reason that has nothing to do with the code under test. Put the gate first so its exit code is what decides. -
Store the token in your CI provider’s secret store, never in the repository. There is no git config key for it on purpose: a bearer token in a checkout is one
git add -Aaway from being committed. -
Mint one token per pipeline, so revoking one does not revoke a developer. A seat is a distinct identity, so a pipeline’s token costs a seat.
Branch on the exit code, not on the output
Section titled “Branch on the exit code, not on the output”0 passed1 the policy was breached2 the gate could not evaluate3 the runner is missing something Bravos needs2 is the one that matters most in a pipeline. 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. Bravos keeps them apart
deliberately: a run that died before it wrote a ledger answers 2, not 0.
bravos: run '20260910-010702-virgin' never produced a ledger, so there is nothing to gate on — it did not get past preflight. Run one that completes (`bravos auto <repo> --attestation ...`), or name a finished run with --run (run_id='20260910-010702-virgin')A finished run that found nothing still passes. The predicate is whether a ledger was written, not what is in it.
Machine-readable output carries the same decision:
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": []}exit_code is in the payload as well as on the process, so a step that captured
stdout does not have to have kept $?. See
Gate a build on a run for the policy
file and what fail_on chooses between.
--reporter github
Section titled “--reporter github”bravos ci --reporter github renders the verdict onto GitHub Actions’ own
surfaces, and is the default when Bravos detects it is running inside Actions.
--reporter none turns that off.
Running the loop in the pipeline
Section titled “Running the loop in the pipeline”The steps above gate on a ledger somebody else produced. Running the loop inside the pipeline is the licensed part of Bravos, and it exists because two things about a pipeline make a local recipe the wrong answer:
- A job that installs cxg, guardlink, Playwright and a headless Chromium pays several minutes on every build.
- A gate whose own toolchain floats is not a gate. A scanner that changes version between two builds makes their two verdicts incomparable.
Both are answered by one artifact: ghcr.io/bugb-technologies/bugb-ci, a runner
image with every component pinned.
| Component | Pinned to |
|---|---|
| base | ubuntu:24.04, by manifest digest |
cxg |
v1.3.0, the public release binary, SHA256-verified against the release’s own SHA256SUMS |
| the cxg pentest pipeline | materialised at build time by cxg pentest install |
playwright and Chromium |
the version that materialised pipeline’s own requirements.txt names |
guardlink |
2.0.0, from public npm |
bravos |
a pinned commit, installed from source into the image |
It runs as a non-root user with a real writable $HOME, because cxg’s auth
store is created 0700 on first use and the --ci guard refuses a directory
with any other-user bit set.
The image is published to a private registry, the same one the server image
lives in, and pulling it needs a credential that comes with your licence. The
tag is pinned in every template, never latest: a gate that pulls a different
runner tomorrow is not reproducible.
Related
Section titled “Related”- Gate a build on a run: the policy the gate reads, and the three exit codes in full.
- Connect the CLI to a server: the push step, and every failure it can report.
- The CLI, the server and CI: which pieces a licence pays for.

