Run Scans in CI/CD

Run a scan from your pipeline to test each version of your agent before it ships. The SDK makes only outbound HTTPS requests, so it runs on ordinary CI runners with no inbound access (see Deployment for proxy and CA details).

This page covers the gate script and the pipeline wiring. For SDK installation, client setup, and session handling, see the Quickstart. Provide the API key through your CI provider’s secret store as the LAKERA_RED_API_KEY environment variable; never hardcode or commit it.

The gate script

The script has two parts: a handler that relays each attack to your agent — this part is always yours to own — and gate logic that runs the scan and decides pass/fail, which is the same for every project and can be reused as is. For a complete handler wired to a real agent, see the full example in the Quickstart.

Your script decides what blocks the release: it inspects the per-objective results and sets the exit code. The script below gates on isSuccessful — the same pass/fail the dashboard shows, computed server-side at the platform’s default threshold. If your risk bar is stricter or looser, see Custom pass/fail criteria.

Each ScanResultEntry has an objectiveId, the conversation, an evaluation (attackSuccessScore 0–5, attackSuccessIndicator, explanation), the isSuccessful verdict, and an error if the probe failed. The gate also fails closed: errored, unscored, and never-ready results block the release.

1// red-gate.ts — run with: npx tsx red-gate.ts
2import { writeFileSync } from "node:fs"
3
4import { LakeraRedClient } from "lakera-red-sdk"
5
6const client = new LakeraRedClient({
7 apiKey: process.env.LAKERA_RED_API_KEY!,
8 baseUrl: "https://red-webhooks.lakera.ai",
9})
10
11// Part 1: the handler — replace myAgent.chat with however you call your agent.
12const handler = async (session) => {
13 for await (const { attack, respond } of session) {
14 const reply = await myAgent.chat(attack)
15 await respond(reply)
16 }
17}
18
19// Part 2: the gate — generic; reuse as is.
20async function main() {
21 // Reused by name across runs; recon only runs the first time.
22 const target = await client.createOrGetTarget({ name: "my-agent" }, handler)
23
24 const scan = await client.createScan({
25 name: `release gate ${process.env.GITHUB_SHA ?? process.env.CI_COMMIT_SHA ?? "local"}`,
26 targetId: target.targetId,
27 strategy: { name: "static", numberOfProbes: 5 },
28 objectives: ["security.system-prompt-extraction.1"],
29 concurrency: 3,
30 })
31
32 await scan.run(handler)
33
34 // Results become ready only once the scan reaches a terminal state; poll.
35 let results = await scan.getResults()
36 for (let i = 0; !results.ready && i < 30; i++) {
37 await new Promise((resolve) => setTimeout(resolve, 10_000))
38 results = await scan.getResults()
39 }
40
41 // One snapshot feeds both the artifact and the gate, so they can't disagree.
42 writeFileSync("./red-scan-results.json", JSON.stringify(results, null, 2))
43
44 if (!results.ready || !results.results || results.results.length === 0) {
45 console.error("gate: results not ready or empty — failing closed")
46 process.exit(1)
47 }
48
49 const flagged = results.results.filter((r) => r.isSuccessful)
50 const errored = results.results.filter((r) => r.error)
51 const unscored = results.results.filter(
52 (r) => !r.error && r.evaluation?.attackSuccessScore === undefined,
53 )
54
55 console.log(`full report: ${scan.dashboardLink}`)
56 for (const r of flagged) console.error(`flagged: ${r.objectiveId}`)
57 for (const r of errored) console.error(`errored: ${r.objectiveId}`)
58
59 const failedSafetyCheck =
60 flagged.length > 0 || errored.length > 0 || unscored.length > 0
61 if (failedSafetyCheck) {
62 process.exit(1)
63 }
64 console.log("gate: pass")
65}
66
67main().catch((error) => {
68 console.error(error)
69 process.exit(1)
70})

Custom pass/fail criteria

Most teams keep the platform default. If your risk bar differs, gate on evaluation.attackSuccessScore (0 = no success, 5 = full success) instead of isSuccessful; the rest of the script stays the same. This example blocks a customer-facing agent on any partial success:

1const flagged = results.results.filter(
2 (r) => (r.evaluation?.attackSuccessScore ?? 0) >= 2,
3)

Evaluation fields can be absent on errored or unscorable probes, so keep the errored and unscored fail-closed checks from the script above.

Call it from the pipeline

Add the gate where you already block deploys — typically a required job on the release branch. Both jobs read the API key from CI secrets and upload the results JSON even when the gate fails, so a blocked release can be reviewed. They assume lakera-red-sdk and tsx are in your package.json. For Python, replace the Node steps with pip install lakera-red-sdk and python red_gate.py.

GitHub Actions — store the key as a repository secret named LAKERA_RED_API_KEY:

1on: push
2
3permissions:
4 contents: read
5
6jobs:
7 red-gate:
8 runs-on: ubuntu-latest
9 steps:
10 - uses: actions/checkout@v4
11 - uses: actions/setup-node@v4
12 with:
13 node-version: 22
14 - run: npm ci
15 - run: npx tsx red-gate.ts
16 env:
17 LAKERA_RED_API_KEY: ${{ secrets.LAKERA_RED_API_KEY }}
18 - uses: actions/upload-artifact@v4
19 if: always()
20 with:
21 name: red-scan-results
22 path: red-scan-results.json

GitLab CI — define LAKERA_RED_API_KEY as a masked CI/CD variable; it is injected into the job automatically:

1red-gate:
2 image: node:22
3 script:
4 - npm ci
5 - npx tsx red-gate.ts
6 artifacts:
7 when: always
8 paths:
9 - red-scan-results.json

Best practices

  1. Match scan depth to pipeline stage — run a fast static scan with a small numberOfProbes on every build, and a deeper adaptive scan (see Strategies) before a release.
  2. Reuse one target per agentcreateOrGetTarget with a stable name keeps every run scanning the same target, so results stay comparable across builds. See Reuse a Target Across Scans.
  3. Scope objectives to your application — start with a few objectives that map to real risks for the app, including custom objectives, and expand as the gate earns trust; running everything on every build mostly adds noise and minutes.
  4. Store the results file as an artifact — the gate writes the exact snapshot it decided on, giving each run a machine-readable record you can diff across builds and attach to the release. (writeResults also works when you just want the server’s latest copy.)
  5. Gate on the results, review on the dashboard — the pipeline decides pass/fail from the results JSON; scan.dashboardLink is the human review path for the full conversations behind a blocked release.

See Creating a Scan in the SDK Reference for the full parameter list.