CI/CD Integration

brunnr is designed for CI pipelines. The scanner requires no API keys, no network access, and no third-party dependencies. It runs in milliseconds and uses deterministic exit codes.


GitHub Actions

name: Skill Security Scan

on:
  push:
    paths:
      - 'skills/**'
  pull_request:
    paths:
      - 'skills/**'

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install brunnr
        run: pip install brunnr

      - name: Scan skills
        run: brunnr scan --strict --json > scan-results.json

      - name: Upload results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: scan-results
          path: scan-results.json

Full pipeline (scan + eval)

name: Skill Pipeline

on:
  push:
    paths:
      - 'skills/**'
      - 'tests/**'
  pull_request:
    paths:
      - 'skills/**'
      - 'tests/**'

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install brunnr
        run: pip install brunnr

      - name: Security scan
        run: brunnr scan --strict

  eval:
    needs: scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install brunnr with eval support
        run: pip install brunnr[eval]

      - name: Run evaluations
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: brunnr pipeline

API key required for evals

The eval step requires an ANTHROPIC_API_KEY secret in your repository settings. The scan step does not require any secrets.


Scan-only pipeline

For projects that don't need LLM evaluation, use the scan-only mode:

- name: Security scan
  run: brunnr pipeline --scan-only

This is the fastest option. No API calls, no secrets, runs in under a second.


Docker

Minimal Dockerfile

FROM python:3.12-slim
RUN pip install --no-cache-dir brunnr
WORKDIR /workspace
ENTRYPOINT ["brunnr"]

Usage

# Build
docker build -t brunnr .

# Scan a local directory
docker run --rm -v "$(pwd)/skills:/workspace/skills" brunnr scan

# Full pipeline
docker run --rm \
  -v "$(pwd):/workspace" \
  -e ANTHROPIC_API_KEY \
  brunnr pipeline

Exit codes reference

All brunnr commands use consistent exit codes suitable for CI:

Command Exit 0 Exit 1
brunnr scan All CLEAN (or FLAG without --strict) Any BLOCK (or FLAG with --strict), or no files
brunnr scan --strict All CLEAN Any BLOCK or FLAG
brunnr eval All assertions pass Any assertion fails
brunnr pipeline Scan passes and evals pass Scan BLOCK or eval failure
brunnr pipeline --scan-only Scan passes Scan BLOCK or no files

JSON output for CI tools

Use --json with brunnr scan for machine-readable output:

brunnr scan --json --strict
{
  "total": 3,
  "blocked": 0,
  "flagged": 1,
  "clean": 2,
  "skills": [
    {
      "file": "skills/ax-rubric/SKILL.md",
      "severity": "clean",
      "blocked": false,
      "flagged": false,
      "finding_count": 0,
      "findings": []
    },
    {
      "file": "skills/infra-setup/SKILL.md",
      "severity": "flag",
      "blocked": false,
      "flagged": true,
      "finding_count": 1,
      "findings": [
        {
          "threat_class": "semantic_mismatch",
          "severity": "flag",
          "description": "sensitive path reference without security context",
          "evidence": "~/.ssh/",
          "line": 42
        }
      ]
    }
  ]
}

You can parse this with jq in your CI scripts:

# Fail if any blocked skills
brunnr scan --json | jq -e '.blocked == 0'

# Get list of flagged files
brunnr scan --json | jq -r '.skills[] | select(.flagged) | .file'

Pre-commit hook

Add brunnr as a pre-commit check for skill files:

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: brunnr-scan
        name: brunnr security scan
        entry: brunnr scan --strict
        language: python
        additional_dependencies: ['brunnr']
        files: 'skills/.*\.md$'
        pass_filenames: true

Or as a simple git hook:

#!/bin/sh
# .git/hooks/pre-commit

if git diff --cached --name-only | grep -q '^skills/'; then
  brunnr scan --strict
fi

GitLab CI

skill-scan:
  image: python:3.12-slim
  script:
    - pip install brunnr
    - brunnr scan --strict --json > scan-results.json
  artifacts:
    reports:
      dotenv: scan-results.json
  rules:
    - changes:
        - skills/**