CI/CD Code Gates for AI-Generated Code

A code gate is a pipeline step that blocks bad code before it ships. This guide covers the three-stage gate architecture for AI-generated Python — pre-commit, CI scan, merge gate — what each stage catches, how to wire each one, and how the audit trail ties them together.

AI coding assistants generate Python that looks right and fails later: in the unbounded loop that hangs under production load, in the hardcoded API key that ships in a string literal, in the hallucinated import that installs cleanly and fails at runtime. A code gate catches these before they reach production. The gate doesn't replace AI-assisted development — it's the safety margin that makes shipping AI-generated code predictable.

This guide uses BrassCoders as the scanner at each gate stage. BrassCoders runs 12 scanners (Bandit, Pylint, Pyre/Pysa, Semgrep, ast-grep, detect-secrets, plus six custom detectors), exits with code 1 on any CRITICAL finding, and makes zero outbound network calls in offline mode. Each gate stage relies on these properties: the exit code for gate enforcement, the offline guarantee for regulated environments, and the reproducible output for audit records.

Why Three Stages, Not One

BrassCoders's three-stage gate architecture runs the same scan at three pipeline points — pre-commit (local, before the repository ever sees the code), CI (on every push and PR, automated and logged), and merge gate (branch protection, requires CI to pass before merging) — because each stage catches a different failure mode at a different cost.

A single CI gate catches everything but costs more per catch: the code is already in the repository, the PR is already open, and if the gate fails, the developer has to push a fix commit. A pre-commit hook catches issues before they enter the repository, at the lowest possible cost — nothing needs to be reverted, no PR comment documents the bug, and the feedback loop is seconds instead of minutes. The merge gate is the enforcement layer: it makes the CI check non-bypassable via branch protection rules, turning advisory into required.

The full pipeline:

git commit
  → pre-commit hook (BrassCoders --offline scan)
  → [CRITICAL found: commit blocked]
  → [clean: commit proceeds]

git push → CI job (BrassCoders --offline scan)
  → [CRITICAL found: CI fails, PR blocked by branch protection]
  → [clean: CI passes, merge enabled]

PR merge
  → merge gate (branch protection requires CI check)
  → [override: exception documented in PR]

Stage 1: The Pre-Commit Gate

BrassCoders configured as a pre-commit hook runs brasscoders --offline scan before each git commit completes. The hook exits non-zero on CRITICAL findings, blocking the commit. The check adds 10-30 seconds to the commit flow and prevents secrets, insecure patterns, and hallucinated imports from entering the repository at all.

Install the pre-commit framework and configure the hook:

# Install dependencies
pip install pre-commit brasscoders==2.0.8

# Create or update .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: brasscoders
        name: BrassCoders scan
        entry: brasscoders --offline scan
        language: system
        pass_filenames: false
        stages: [pre-commit]

# Install the hook
pre-commit install

The pass_filenames: false option tells the pre-commit framework to run BrassCoders against the full project directory rather than individual staged files — which is necessary because BrassCoders performs project-level analysis (taint flows, cross-file secret patterns, import resolution) that requires the full tree.

Pre-commit hooks can be bypassed with git commit --no-verify. Document the team policy for when this is acceptable: the recommended pattern is that bypass is allowed for WIP commits to feature branches, never for commits directly to main or release branches. The CI gate at Stage 2 catches anything that bypasses the pre-commit hook.

Stage 2: The CI Gate

BrassCoders in GitHub Actions runs on every push and every pull request, produces a .brass/ artifact for review, and fails the CI job on CRITICAL findings. The CI gate is the authoritative record: unlike the pre-commit hook, it can't be bypassed with a flag, it runs on every contributor's code (including external PRs), and its output becomes the audit trail.

Full workflow configuration:

name: BrassCoders Scan
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  brasscoders:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Install BrassCoders
        run: pip install brasscoders==2.0.8
      - name: Run BrassCoders scan
        run: brasscoders --offline scan .
      - name: Upload .brass artifact
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: brasscoders-findings
          path: .brass/
          retention-days: 90

The if: always() on the artifact upload step ensures the .brass/ directory is uploaded even when the scan fails — so reviewers can see what was found, not just that something was found. The retention-days: 90 sets a 90-day artifact retention for audit purposes; adjust to match your compliance requirements.

For teams using the Paid plan, set BRASS_LICENSE_KEY as a GitHub Actions secret and add it to the environment:

      - name: Run BrassCoders scan
        run: brasscoders scan .
        env:
          BRASS_LICENSE_KEY: ${{ secrets.BRASS_LICENSE_KEY }}

Omit the --offline flag when using the Paid plan — the enrichment pass requires a network call to BrassCoders's gateway. The gateway receives already-redacted findings and a project signature; never raw source code.

Stage 3: The Merge Gate

The merge gate converts the CI check from advisory to required by configuring GitHub branch protection rules to block the merge button until the BrassCoders CI job passes. Without this step, a developer can merge a PR with a failing CI scan — the CI result becomes a warning, not a gate.

Configure branch protection in GitHub repository settings:

  1. Navigate to Settings → Branches → Branch protection rules.
  2. Add a rule for your default branch (main, master, or your branch naming convention).
  3. Enable "Require status checks to pass before merging."
  4. Search for and add "brasscoders" (the CI job name from your workflow) as a required check.
  5. Enable "Require branches to be up to date before merging" to prevent stale-PR merges that bypass the check.

Once configured, the GitHub merge button is grayed out until the BrassCoders check shows a green checkmark. A PR with a CRITICAL finding cannot be merged — the developer must fix the finding and push a new commit, or document an exception in the PR description and have an admin override the branch protection.

For teams using GitLab CI, the equivalent configuration is a protected branch with "Pipelines must succeed" enabled. The BrassCoders scan step's exit code drives the pipeline status.

The Audit Trail

The .brass/detailed_analysis.yaml file produced by each CI run contains every finding from every scanner — file path, line number, severity, scanner source (Bandit rule ID, detect-secrets pattern name, BrassCoders custom rule, etc.), finding title, and evidence string. Uploaded as a GitHub Actions artifact at a 90-day retention, it's a reproducible, timestamped audit record. The same commit hash plus the same BrassCoders version produces identical output.

For SOC 2 or similar audits: the artifact history in GitHub Actions provides a scannable record of what was scanned, when, at what version, and what was found. The brasscoders==2.0.8 pin in the workflow ensures the scan version is fixed across the audit period; pin upgrades appear in the git history.

The three audit-relevant files in .brass/:

  • ai_instructions.yaml — short, severity-ranked list for AI assistant triage (see next section)
  • detailed_analysis.yaml — every finding with full evidence; the audit artifact
  • security_report.yaml — security-only filtered view, suitable for a security team review

Triaging Findings with an AI Assistant

BrassCoders's ai_instructions.yaml output is designed to be pasted directly into Claude Code or Cursor for triage. The file is a short, severity-ranked list — typically 20-50 findings for the Paid tier, more for the OSS core — that fits in a single AI assistant message alongside a triage prompt.

A triage workflow that takes under 5 minutes per PR:

  1. Download the brasscoders-findings artifact from the CI run.
  2. Open ai_instructions.yaml.
  3. Paste it into Claude Code or Cursor with: "These are static analysis findings from this PR. Which are real issues vs false positives? For each real issue, what's the fix?"
  4. Apply the fixes the AI confirms are real. Mark false positives with a .brassignore entry.

This division of labor — BrassCoders identifies patterns deterministically, an AI assistant applies context to classify real vs false positive — is the intended workflow. BrassCoders doesn't infer context (a pattern match is a pattern match regardless of the variable name). The AI assistant does infer context (an MD5 use in a hash table key is different from an MD5 use in a cryptographic context). Each does the part it's suited for.

The Complete Gate Configuration

Summary of files to add or update for the full three-stage pipeline:

  • .pre-commit-config.yaml — pre-commit hook configuration (Stage 1)
  • .github/workflows/brasscoders-scan.yml — CI workflow (Stage 2)
  • GitHub branch protection rules — merge gate (Stage 3, configured in repository settings)
  • .brassignore — project-specific exclusions (false positives, test fixtures, generated files)

A minimal .brassignore for most Python projects:

# .brassignore — BrassCoders exclusion file
# Syntax: same as .gitignore

# Test fixtures with intentional bad patterns
tests/fixtures/

# Generated code
*_pb2.py
*_pb2_grpc.py

# Vendor copies
vendor/

Once all three stages are configured, the gate pipeline runs automatically on every commit and PR. The pre-commit hook gives developers fast local feedback, CI gives the team the authoritative check and audit record, and branch protection makes the result enforceable.

Frequently Asked Questions

What is a code gate in CI/CD?

A code gate is a step in the CI/CD pipeline that must pass before code can proceed to the next stage. For static analysis, a gate runs a scanner, checks the exit code, and blocks the pipeline on failure. BrassCoders exits with code 1 on any CRITICAL finding — that exit code is what a GitHub Actions step, GitLab CI step, or similar CI system uses to fail the job and prevent a merge.

Should I use pre-commit or CI, not both?

Both. Pre-commit is the fast local catch — it runs in 10-30 seconds before git commit succeeds and prevents secrets and critical findings from entering the repository at all. CI is the enforced gate — it runs on every push and PR and can't be bypassed with --no-verify the way pre-commit can. The pre-commit hook catches issues cheapest (nothing in the repo yet); CI catches anything that bypasses the hook and produces the audit artifact.

What does BrassCoders exit with on a clean scan?

BrassCoders exits with code 0 when no CRITICAL findings are detected. Non-critical findings (MEDIUM, LOW, INFO) are written to .brass/ but don't affect the exit code. A scan that exits 0 with non-critical findings still uploads the .brass/ artifact for review — the gate doesn't mean the scan was finding-free, only that no blocking-severity issues were found.

How do I set up the merge gate in GitHub?

In GitHub repository settings, go to Branches → Branch protection rules → Add rule for main (or your default branch). Enable "Require status checks to pass before merging" and add the BrassCoders CI job name as a required check. Once configured, GitHub blocks the merge button until the BrassCoders scan exits 0. The job name must match exactly what appears in your Actions workflow.

What's in the .brass/detailed_analysis.yaml audit artifact?

Every finding from every scanner, with: file path, line number, severity (CRITICAL/HIGH/MEDIUM/LOW/INFO), scanner source (which of the 12 scanners produced it), rule ID, finding title, and evidence string (the code that triggered the finding). The file is deterministic — the same codebase + same BrassCoders version produces identical output. Uploaded as a CI artifact, it's a reproducible audit record tied to the commit hash.

How do I use .brass/ai_instructions.yaml for triage?

Download the .brass/ai_instructions.yaml artifact from the CI run and paste it into Claude Code or Cursor with a prompt like "These are static analysis findings from this PR. Which are real issues vs false positives, and what's the fix for each real one?" The file is formatted as a short, severity-ranked list — typically 20-50 findings for the Paid tier, more for the OSS core — designed to fit in a single AI assistant message.