cli.sgit.ai / Research / Security CI: a dedicated pipeline, and an AI review that can break the build

Security CI: a dedicated pipeline, and an AI review that can break the build

Date2026-10-13 · 12:49 UTCKinddesignTopicssecurity ci ai-reviewWritten bythe sgit-ai CLI team (Claude Code sessions), for Dinis CruzSourceteam/humans/dinis_cruz/claude-code-web/10/13/security-ci__pipeline-and-ai-review.md in SGit-AI__CLI at 5597526. Published here as written; the CLI repository is where the work happened.

Your questions (13 Oct):

  1. Is a dedicated security pipeline worth it?
  2. Can some of the checks and AI security reviews run in CI and break the build on anything new?
  3. What would that cost, on a cheaper model?
  4. How do you create a Claude key for it?

Short answers:

  1. Yes. It is now .github/workflows/security.yml.
  2. Yes. The AI review is built, tested without spending anything, and switched off until you connect it. It is keyless: GitHub's OIDC token through workload identity federation.
  3. About $0.20–0.65 per reviewed PR on Sonnet 5.5, or 1–16 cents on Haiku 5.5. Changes that touch none of the 45 security-slice files cost nothing.
  4. No key is needed: workload identity federation, steps in §6.

1. Why a dedicated pipeline

Three reasons:

The release pipeline keeps its own security-tests job, since that job gates publishing. The new workflow does not replace it.

2. The gates (each fails only on what is new)

JobWhat it runsFails when
Security & adversarial testspytest tests/securitya Fixed__* attack works again, or a Known_Gaps gap moved
Bandit (baseline)bandit -r sgit_ai -b tests/security/bandit-baseline.json (1.9.4)a finding not in the committed baseline
Semgrep (new lines)p/python, p/security-audit, p/secrets, r/python.cryptography (1.178.0) on sgit_ai and review/tools, --baseline-commit = the PR's basea finding on a line the PR adds or moves. The weekly run reports everything as SARIF and does not fail
pip-audit--strict on the runtime dependencies, now read from pyproject.toml (the release job hardcodes them)any known advisory
zizmorthe workflows and the composite action, --min-severity medium, ignores in .github/zizmor.ymla new medium or high finding
actionlintactionlint (1.7.12) on the workflowsa workflow GitHub would reject. Added after the first dev run of this change failed at load: a duplicated with: in the mutation job passed every YAML parser and zizmor
review/ graph currentreview/tools/derive.py --checknever fails: a warning, because a stale graph is a docs debt, not a defect
AI security reviewreview/tools/ai_review.py (§4)a new, confident, medium-or-worse finding that survives a second opinion

Calibrated here:

3. What zizmor found in our workflows, and what changed

Risk: these edits touch the release pipeline's test action. They parse, zizmor is clean, and the word-splitting is checked. The first dev run after the merge is the real check, so watch it.

4. The AI review (review/tools/ai_review.py)

Flow

  1. Scope. Take the PR's changed files that are in the security slice (review/slice.json, 45 files: crypto, host and local boundaries, verify-then-accept, key stores, supply chain). If there are none, stop: it costs nothing.
  2. Request. One request to Claude:
  1. Findings. Each finding has a file and line, a function, a category, a severity, a confidence, and register: the threat-model row it is (TM-F/TM-R), or new.
  2. The gate.
  1. Waivers. You accept a finding by adding its id (12 hex: file, function and category, not the wording) with a reason to review/findings/ai-waivers.json. Exit codes:

| Code | Meaning | |---|---| | 0 | pass | | 1 | new findings | | 2 | error | | 3 | inconclusive: the model declined or ran out of tokens; nothing was reviewed, which is not a pass |

What it is not

The diff is attacker-controlled text and the model reads it. A hostile change can argue its way past the reviewer. So the AI review catches mistakes by honest contributors and agents; the deterministic gates are the controls. The model gets no tools, so injected text can only change the verdict. The prompt also asks it to report steering text as a finding.

The job runs only:

Never move it to pull_request_target.

Tested without spending anything

The off-the-shelf alternative

anthropics/claude-code-security-review runs Claude Code as an agent over the PR (up to 20 minutes) and comments on it.

It is good for PR comments. As a gate, ours is cheaper and predictable, because it is one call with no tools and known inputs. It also knows our threat model and register, so it can tell new from known. Both can run if you want PR comments as well.

5. Cost (measured inputs, list prices as of 2026-10)

Dry runs (--dry-run: counts what would be sent, sends nothing) on five real commits:

CommitSlice filesInput tokens (~)Sonnet 5.5 clean / with findingsHaiku 5.5 clean / with findingsOpus 5.5 clean / with findings
b32a6ba sgit serve261k$0.19 / $0.32$0.01 / $0.02$0.38 / $0.64
9362a25 TM-F32..36475k$0.22 / $0.38$0.01 / $0.02$0.44 / $0.76
fbd550f pull guard475k$0.22 / $0.38$0.01 / $0.02$0.44 / $0.76
eb7941c bulk fetch4109k$0.29 / $0.51$0.07 / $0.13$0.57 / $1.03
a5e6043 partial clones7144k$0.36 / $0.65$0.09 / $0.16$0.71 / $1.30

How the numbers are built:

Per month, at 60 PRs that touch the slice:

ModelPer monthBefore your first top-upSpend limit to set
Sonnetabout $15–40a $10 credit covers about 30 reviews$25
Haikuabout $1–5

Which model.

6. Switching it on: keyless, with workload identity federation

Updated 13 Oct. This replaces the API-key setup first written here.

The CI job holds no API key. It asks GitHub for a short-lived OIDC token (about 5 minutes) for the audience https://api.anthropic.com, and the SDK exchanges that token once for an Anthropic token bound to a service account. Billing goes to the organization that owns the service account (the one with the credits), attributed to the rule's workspace.

The job runs on every push to dev, since that is where changes land here. It can also run on PRs. It reviews before..after of the push. A push that touches no security-slice file (release-badge commits, docs) is skipped at no cost.

In the Claude Console (organization admin)

  1. Settings → Workspaces → Create workspace sgit-ci. Click the workspace, then Spend limits, and set the monthly cap, for example $25 with an alert at 50%.
  2. Settings → Workload identity → Connect workload → GitHub Actions. The wizard creates the issuer, the service account and the rule together. It is better than the bare Register issuer dialog.
  1. Note the ids: rule fdrl_…, service account svac_…, workspace wrkspc_…, and the organization id (Settings → Organization).

In GitHub (Settings → Secrets and variables → Actions → Variables tab)

None of these values is a secret: without a GitHub token that matches the rule, they mint nothing.

VariableValue
SECURITY_AI_REVIEWtrue
ANTHROPIC_ORGANIZATION_IDthe organization id
ANTHROPIC_SERVICE_ACCOUNT_IDsvac_…
ANTHROPIC_WORKSPACE_IDwrkspc_…
ANTHROPIC_FEDERATION_RULE_IDthe dev-push rule, fdrl_…
ANTHROPIC_FEDERATION_RULE_ID_PRoptional: the PR rule
SECURITY_AI_REVIEW_MODELoptional: claude-haiku-5-5 (the default is Sonnet 5.5)

Do not add an ANTHROPIC_API_KEY secret. A set key, even an empty one, takes precedence over federation; the job unsets it anyway.

Who can spend. Anyone who can push to dev can run a workflow that mints a token. Forks cannot: a fork's PR gets no OIDC token. The workspace spend limit is the backstop.

Tested here without spending anything. I ran the job's exact shell step against a local stand-in for both GitHub's token endpoint and Anthropic's token exchange:

ai_review.py now makes one SDK client per run. Before this fix, the second call created a new client, which would have re-presented the single-use GitHub token and been refused (jti_reused).

Locally, with your own key and never in a repo file, the commands in §5 still work: export ANTHROPIC_API_KEY=… and python3 review/tools/ai_review.py --base 9362a25 --head 9362a25^.

7. Next steps (not done here)

This page as markdown · its source, admin/content/articles/security-ci-and-ai-review.md