Run a pentest in CI
A pentest in CI has a failure mode a scan does not: the saved session expires, the run probes the app logged out, and every real finding comes back refuted. CI mode turns that silent miss into a hard failure. This guide restores a session from a secret, verifies it, and runs the pipeline so a dead session stops the run before it wastes any AI budget.
Before you start
Section titled “Before you start”cxgand the pentest orchestrator installed. See the web-application pentest.- A captured session exported once, locally, as a Playwright
storage_state. That artifact is what CI restores; nobody logs in during the run.
Restore the session into an isolated directory
Section titled “Restore the session into an isolated directory”A saved session is a credential. Keep it out of the operator’s home store and in
a directory the run owns, mode 0700. cxg reads the state from a file, from
stdin, or from a base64 environment variable. The last is what a CI secret store
gives you.
-
Create the auth directory with tight permissions.
Terminal window mkdir -p ci-auth && chmod 700 ci-auth -
Import the session. In CI, pipe the secret straight in rather than writing it to disk in the clear.
Terminal window printf '%s' "$SESSION_STATE" | cxg pentest auth import \--profile live --target https://staging.example.com \--storage-state - --auth-dir ./ci-auth --ci--storage-state -reads stdin.--cimakes import refuse a world-readable destination rather than write a credential a fellow user could read:✗ auth dir ci-auth is world-accessible (mode 0o755); a saved session is a credential. Run `chmod 700 ci-auth` and retry, or run without CI mode to override.That refusal exits
2. It comes fromauth import, not fromrun, so guard it on the import step rather than expecting the run to report it.
Verify before you spend a run
Section titled “Verify before you spend a run”auth verify is a cheap liveness gate: exit 0 if the session is alive,
non-zero if it is dead. Run it first and skip the expensive pipeline when it
fails.
cxg pentest auth verify --profile live --auth-dir ./ci-auth ✓ session for 'live' is ALIVE against https://staging.example.com · landed at https://staging.example.com/ (no login redirect)A dead session prints ✗ session for live expired with the landing page it hit,
and exits 1, so the guard is:
cxg pentest auth verify --profile live --auth-dir ./ci-auth \ || { echo "session expired, re-import"; exit 1; }Run with the CI gate
Section titled “Run with the CI gate”Pass --ci to run, or set CXG_CI=1 for pipelines that cannot change the
invocation. Now a dead session at pre-flight is a hard failure instead of a
warn-and-continue:
cxg pentest run \ --codebase ./app --target https://staging.example.com \ --auth live --auth-dir ./ci-auth \ --ai --ai-provider claude \ --attestation "$CI_ENGAGEMENT_ATTESTATION" \ --ciAgainst a dead session the run stops at the identity check:
[1b] inspecting 1 captured auth profiles… · live (DEAD: status=401) · landed at https://staging.example.com/login which looks like a login/SSO page · /api/me returned 401and exits 5, before generating or sending anything. Without --ci the same
situation only warns and probes on, logged out.
Two ways the gate stops protecting you
Section titled “Two ways the gate stops protecting you”--ci only helps if the pre-flight check can tell a dead session from a live
one. Two things defeat it, and both are silent.
Wire the exit codes into the job
Section titled “Wire the exit codes into the job”The run’s exit code is the whole verdict. These are every code cxg pentest run
returns:
| Exit | Meaning | CI action |
|---|---|---|
0 |
Clean. No confirmed findings. | Pass. |
1 |
No templates were available to run. | Investigate. Usually a missing findings.sarif. |
2 |
Confirmed findings present. | Fail the build, publish report.json. |
3 |
Hard-killed: a 5xx streak, a scope violation, or under --no-restart a desktop target that died mid-scan. |
Investigate the target. |
5 |
CI mode: the session was dead at pre-flight. | Fail loud, re-capture the session. |
There is no exit 4. The point of --ci is that 5 exists at all: without it,
an expired session passes quietly as a 0.
What to change next
Section titled “What to change next”- Record authorization.
--attestationwrites your engagement ID and operator into the audit-log header, the dispute-ready record that you had permission. Strongly recommended for any non-local target. - Bound the AI cost.
--max-templatescaps how many probes the ranker may select, and--generation-timeoutcaps each AI call.
Related
Section titled “Related”- Pentest a web application covers the full pipeline this guide runs headless.
cxg pentesthas every flag.

