GitHub Actions and Pre-commit
Scan skills automatically on every push or pull request with the reusable GitHub Actions workflow, or before every commit with the pre-commit hook. In CI, findings appear as inline annotations on pull requests through GitHub Code Scanning.
Which preset and threshold to use is covered in Recommended Settings. The short version:
- Every setup runs the LLM judge (
use_llm: true). Rules alone catch only about 8% of malicious skills. - For your own skills, use
low-noisewith the judge, failing at HIGH. - For third-party skills, use
balanced(highest F1) orquiet(lowest false-positive rate) with the judge.
Quick Start
Add your model provider's key as the repository secret SKILL_SCANNER_LLM_API_KEY, then add this file
to your repository at .github/workflows/scan-skills.yml:
name: Scan Skills
on:
push:
paths: [".cursor/skills/**"]
pull_request:
paths: [".cursor/skills/**"]
jobs:
scan:
uses: cisco-ai-defense/skill-scanner/.github/workflows/scan-skills.yml@2.2.1
with:
scanner_version: "2.2.1"
skill_path: .cursor/skills
policy: low-noise
fail_on_severity: high
use_llm: true
llm_model: anthropic/claude-sonnet-5-5
secrets:
llm_api_key: ${{ secrets.SKILL_SCANNER_LLM_API_KEY }}
permissions:
security-events: write
contents: read
actions: read
For Bedrock, Vertex AI or a model you host, use the self-hosted workflow with the judge on Bedrock instead.
Pin the workflow to a release tag such as @2.2.1, not @main. The caller must grant the three
permissions shown. A reusable workflow can only use permissions its caller grants, and the SARIF
upload needs security-events: write.
This will:
- Install
cisco-ai-skill-scannerfrom PyPI on a fresh runner (pin the version with thescanner_versioninput) - Run
skill-scanner scan-all --use-llmwith SARIF output, recursive scanning, and cross-skill overlap detection - Upload SARIF results to GitHub Code Scanning (findings appear as annotations on PRs)
- Fail the workflow if any findings at or above HIGH severity are detected
Reusable Workflow Inputs
| Input | Type | Default | Description |
|---|---|---|---|
skill_path | string | (required) | Path to skills directory or single skill |
scan_mode | string | scan-all | scan (single skill) or scan-all (directory) |
format | string | sarif | Output format: summary, json, markdown, table, sarif, html |
policy | string | balanced | Preset (balanced, low-noise, quiet, strict, permissive) or path to custom YAML |
scanner_version | string | "" (latest) | cisco-ai-skill-scanner release to install, e.g. 2.2.0. Pin it to the same release as the workflow ref |
fail_on_severity | string | high | Fail if findings at/above this severity |
python_version | string | 3.12 | Numeric CPython version from 3.11 through 3.14 |
upload_sarif | boolean | true | Upload SARIF to Code Scanning |
use_llm | boolean | false | Enable LLM semantic analysis |
llm_model | string | "" | LLM model, as provider/model (e.g., anthropic/claude-sonnet-5-5) |
llm_provider | string | "" | Provider override, e.g. openai-compatible for a gateway |
llm_base_url | string | "" | Base URL for an OpenAI-compatible gateway |
aws_region | string | us-east-1 | Region for bedrock/ models; pass a Bedrock API key as the llm_api_key secret |
use_behavioral | boolean | false | Enable behavioral dataflow analysis |
lenient | boolean | false | Tolerate malformed skills |
extra_args | string | "" | Additional CLI flags accepted by the workflow's explicit allowlist |
The extra_args allowlist accepts additive analyzer and tuning options. These include
--rule-packs, --use-osv, --use-virustotal, --use-aidefense, --use-trigger,
--llm-decompose, --adjudicate, LLM limits, and repository-local policy and taxonomy paths.
Unknown flags are rejected before the scan runs.
The reusable workflow can reach Bedrock with a Bedrock API key (the llm_api_key secret, a bedrock/
model and aws_region), but it cannot assume an AWS role or another cloud identity. For role-based
Bedrock or Vertex AI, use the self-hosted workflow.
A run that cannot start as configured fails with "could not run as configured" (scanner exit code 2), which is different from "found findings". An unknown policy or a judge without a working key causes it.
Secrets
All secrets are optional and only needed for advanced analysis.
| Secret | Maps To | Required For |
|---|---|---|
llm_api_key | SKILL_SCANNER_LLM_API_KEY | use_llm: true |
virustotal_api_key | VIRUSTOTAL_API_KEY | --use-virustotal (via extra_args) |
Configure secrets in Settings > Secrets and variables > Actions. They are never exposed in logs.
Configuration Tiers
Tier 1: Rules + LLM Judge — your own skills
jobs:
scan:
uses: cisco-ai-defense/skill-scanner/.github/workflows/scan-skills.yml@2.2.1
with:
scanner_version: "2.2.1"
skill_path: .cursor/skills
policy: low-noise
use_llm: true
llm_model: anthropic/claude-sonnet-5-5
secrets:
llm_api_key: ${{ secrets.SKILL_SCANNER_LLM_API_KEY }}
permissions:
security-events: write
contents: read
actions: read
Fails on HIGH; MEDIUM findings appear as annotations for the reviewer.
Tier 2: Rules + LLM Judge — third-party skills
jobs:
scan:
uses: cisco-ai-defense/skill-scanner/.github/workflows/scan-skills.yml@2.2.1
with:
scanner_version: "2.2.1"
skill_path: vendor/skills
policy: balanced # quiet for the lowest false-positive rate
use_llm: true
llm_model: anthropic/claude-sonnet-5-5
secrets:
llm_api_key: ${{ secrets.SKILL_SCANNER_LLM_API_KEY }}
permissions:
security-events: write
contents: read
actions: read
For an OpenAI-compatible gateway, add llm_provider: openai-compatible and llm_base_url.
Tier 3: Judge + Behavioral + VirusTotal
jobs:
scan:
uses: cisco-ai-defense/skill-scanner/.github/workflows/scan-skills.yml@2.2.1
with:
scanner_version: "2.2.1"
skill_path: vendor/skills
use_llm: true
use_behavioral: true
extra_args: --use-virustotal --use-osv
secrets:
llm_api_key: ${{ secrets.SKILL_SCANNER_LLM_API_KEY }}
virustotal_api_key: ${{ secrets.VIRUSTOTAL_API_KEY }}
permissions:
security-events: write
contents: read
actions: read
Keep strict for audits where a person reads every finding; it is not measured as a gate.
Branch Protection
Block PRs with security findings from merging:
- Go to Settings > Branches > Branch protection rules
- Enable Require status checks to pass before merging
- Search for and select the Skill Scanner check
- Save changes
PRs that touch skill files must pass the security scan before they can be merged.
Pre-commit Hook
Scan skills, with the judge, before every commit using the pre-commit framework:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/cisco-ai-defense/skill-scanner
rev: 2.2.1 # the latest release tag (no "v" prefix)
hooks:
- id: skill-scanner
Turn the judge on in .skill_scannerrc at the repository root (use_llm is off by default):
{
"skills_path": ".claude/skills",
"policy": "low-noise",
"use_llm": true,
"llm_model": "anthropic/claude-sonnet-5-5",
"severity_threshold": "high",
"fail_fast": true
}
The hook scans only the skills a commit touches. The key comes from SKILL_SCANNER_LLM_API_KEY,
or from cloud credentials for Bedrock and Vertex AI. llm_model and llm_provider fall back to
SKILL_SCANNER_LLM_MODEL and SKILL_SCANNER_LLM_PROVIDER, so the hook can point at a local model.
If the judge cannot be built, the commit is blocked with exit code 2 instead of passing on the rules
alone. For a bedrock/ model, add additional_dependencies: [boto3] to the hook. Run
pre-commit install once, or skill-scanner-pre-commit --install without the pre-commit framework.
For incremental CI scans, let pre-commit supply the changed paths between two checked-out revisions:
pre-commit run skill-scanner --from-ref "$BASE_SHA" --to-ref "$HEAD_SHA"
Both revisions must be available in the checkout (for example, fetch-depth: 0 with
actions/checkout).
Self-Hosted Workflow
If you prefer not to use the reusable workflow, copy this standalone workflow:
name: Scan Skills
on:
push:
paths: [".cursor/skills/**"]
pull_request:
paths: [".cursor/skills/**"]
jobs:
scan:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install "cisco-ai-skill-scanner==2.2.1"
- name: Scan skills
env:
SKILL_SCANNER_LLM_API_KEY: ${{ secrets.SKILL_SCANNER_LLM_API_KEY }}
SKILL_SCANNER_LLM_MODEL: anthropic/claude-sonnet-5-5
run: |
skill-scanner scan-all .cursor/skills \
--use-llm \
--policy low-noise \
--format sarif \
--output results.sarif \
--recursive \
--check-overlap \
--fail-on-severity high
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: results.sarif
Custom Policy in CI
Commit a custom policy to the repository and reference it:
with:
skill_path: .cursor/skills
policy: .github/scan-policy.yaml
use_llm: true
llm_model: anthropic/claude-sonnet-5-5
Or in the self-hosted workflow:
skill-scanner scan-all .cursor/skills \
--use-llm \
--policy .github/scan-policy.yaml \
--format sarif \
--output results.sarif \
--fail-on-severity high
Self-Hosted Workflow with the Judge on Bedrock
The model the published figures were measured with runs on Bedrock. Assume a role with OIDC, then run the CLI:
jobs:
scan:
runs-on: ubuntu-latest
permissions:
id-token: write
security-events: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.BEDROCK_ROLE_ARN }}
aws-region: us-east-1
- run: pip install "cisco-ai-skill-scanner[bedrock]==2.2.1"
- name: Scan skills
env:
SKILL_SCANNER_LLM_MODEL: bedrock-mantle/google.gemma-4-26b-a4b
run: |
skill-scanner scan-all vendor/skills --recursive --use-llm --policy balanced \
--fail-on-severity high --format sarif --output results.sarif
- uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: results.sarif
If the judge cannot start, for example because the role cannot reach the model, the scan exits with code 2 instead of passing with rules only.