A SKILL.md, AGENTS.md, custom instruction file, or MCP configuration can affect how an agent reads a repository, calls tools, writes code, updates infrastructure, or explains a design. These files may look like documentation, but once an agent uses them to make decisions, they sit inside the trust boundary.
With all this, you could potentially be introducing a new supply-chain surface area. The fastest way to reduce that risk is to scan in CI, on every change, with consistent outputs your team can review.
To assist with the above, i’ve been looking at NVIDIA SkillSpector – It gives you a way to scan skill and instruction content for risky behaviour, then produce output that fits into normal engineering workflows.
In this blog post, I will be showing how to run SkillSpector as part of your GitHub Actions workflow. The pattern is straightforward: scan the files that influence agent behaviour, run the scan in CI, and make the results visible to reviewers.
What to scan with SkillSpector?
SkillSpector can scan a directory, Git repository or even a single markdown file. In practice, I would usually point it at either the whole repository for scheduled baseline coverage, or the specific paths where skills and instruction files live.
Agents, skills and instruction files can hide:
- Prompt-injection patterns
- Exfiltration behavior
- Unsafe tool-usage chains
- Policy-bypass or self-modification tactics
- Risky script snippets
SkillSpector detects these patterns and produces structured findings with severity, recommendation, and output formats like Markdown and SARIF.
Manual scans are useful with SkillSpector but I do recommend adding it to your CI. CI scans are a control.
- Every PR/push is checked automatically.
- Scanning stays consistent over time.
- Findings are captured as artifacts and/or SARIF.
- Reviewers get fast feedback where they already work.
GitHub Actions Setup for SkillSpector
I’ve created two variations:
- Baseline repository scan
- Pull Requests scan that adds comments to the PR
For each, i’ve used Azure AI foundry to assist and created the secrets (Go to Settings → Secrets and variables → Actions → New repository secret):
- OPENAI_API_KEY: key for accessing OpenAI
- OPENAI_BASE_URL: example: https://<foundry_name>.openai.azure.com/openai/v1/
- SKILLSPECTOR_PROVIDER: openai
- SKILLSPECTOR_MODEL: gpt-5.4
Also: Enable GitHub Code Scanning (Settings → Security → Code scanning) to view SARIF results in the Security tab.
GitHub Action Baseline repository scan
This workflow scans the whole repository and writes SARIF.
It also handles SkillSpector exit codes deliberately. Exit code 1 means the scan completed and returned a DO_NOT_INSTALL recommendation. I would treat that as a finding, not a broken workflow. Exit code 2 is the execution/input failure case.
name: SkillSpector Security Scan
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
schedule:
- cron: "0 6 * * 1" # Every Monday at 06:00 UTC
workflow_dispatch:
inputs:
target:
description: "Target to scan (git URL, file path, or leave blank to scan this repo)"
required: false
default: ""
use_llm:
description: "Enable LLM semantic analysis"
required: false
default: "true"
type: choice
options:
- "true"
- "false"
permissions:
actions: read
contents: read
security-events: write # Required to upload SARIF to GitHub Code Scanning; note this has no effect on pull_request runs from forks (GITHUB_TOKEN is read-only there), so SARIF upload is skipped for fork PRs.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
skillspector:
name: SkillSpector Scan
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
- name: Install SkillSpector
# Pinned to a commit SHA: upstream has no tagged releases yet, only branches.
# Re-pin periodically to pick up fixes/updates.
run: uv tool install "git+https://github.com/NVIDIA/skillspector.git@326a2b489411a20ed742ff13701be39ba00063c8"
- name: Determine scan target
id: target
run: |
INPUT="${{ github.event.inputs.target }}"
if [ -n "$INPUT" ]; then
echo "path=$INPUT" >> "$GITHUB_OUTPUT"
else
echo "path=." >> "$GITHUB_OUTPUT"
fi
- name: Run SkillSpector — SARIF (with LLM)
id: scan_sarif
env:
SKILLSPECTOR_PROVIDER: ${{ secrets.SKILLSPECTOR_PROVIDER != '' && secrets.SKILLSPECTOR_PROVIDER || 'openai' }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
SKILLSPECTOR_MODEL: ${{ secrets.SKILLSPECTOR_MODEL }}
run: |
TARGET="${{ steps.target.outputs.path }}"
LLM_FLAG=""
if [ "${{ github.event.inputs.use_llm }}" = "false" ] || [ -z "$OPENAI_API_KEY" ]; then
LLM_FLAG="--no-llm"
echo "LLM disabled (no key or explicitly disabled)"
fi
set +e
skillspector scan "$TARGET" $LLM_FLAG \
--format sarif --output skillspector-results.sarif
SCAN_EXIT=$?
set -e
if [ "$SCAN_EXIT" -eq 2 ]; then
echo "SkillSpector failed to scan due to an execution/input error."
exit 2
fi
if [ "$SCAN_EXIT" -eq 1 ]; then
echo "SkillSpector reported DO_NOT_INSTALL findings; continuing to upload reports."
fi
- name: Upload SARIF to GitHub Code Scanning
if: always()
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
sarif_file: skillspector-results.sarif
category: skillspector
- name: Upload reports as artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: skillspector-reports
path: skillspector-results.sarif
retention-days: 30
- name: Print summary to job log
if: always()
run: |
{
echo "## SkillSpector Report"
echo ""
echo "Full SARIF results uploaded to the Security tab (category: \`skillspector\`, if permissions allow) and attached as the \`skillspector-reports\` workflow artifact."
} >> "$GITHUB_STEP_SUMMARY"
Example of code scanning alert from the above on GitHub:


Focused pull request scan
A repository-wide scan is useful for coverage, but it is not always the best PR experience. If someone changes one skill, reviewers want the result for that skill.
This workflow finds changed skill directories and instruction files, scans those targets, uploads the reports as an artifact, and updates one PR comment for same-repository pull requests.
name: SkillSpector PR Skills Scan
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- ".github/workflows/skills/**"
# Add one entry per root listed in TARGET_ROOTS below so the workflow
# only triggers when files under those paths actually change.
# - "skills/**"
# - "agents/skills/**"
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
skillspector-pr-skills:
name: Scan changed skill folders
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
- name: Install SkillSpector
# Pinned to a commit SHA: upstream has no tagged releases yet, only branches.
# Re-pin periodically to pick up fixes/updates.
run: uv tool install "git+https://github.com/NVIDIA/skillspector.git@326a2b489411a20ed742ff13701be39ba00063c8"
- name: Scan changed skill directories and build PR report
id: scan
env:
SKILLSPECTOR_PROVIDER: ${{ secrets.SKILLSPECTOR_PROVIDER != '' && secrets.SKILLSPECTOR_PROVIDER || 'openai' }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
SKILLSPECTOR_MODEL: ${{ secrets.SKILLSPECTOR_MODEL }}
run: |
BASE_SHA="${{ github.event.pull_request.base.sha }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
# One or more roots to scan for changed skill folders. Keep this in
# sync with the `paths:` filter above.
TARGET_ROOTS=(".github/workflows/skills")
mkdir -p skillspector-pr-reports
mapfile -t CHANGED_FILES < <(git diff --name-only --diff-filter=ACMR "$BASE_SHA" "$HEAD_SHA" -- "${TARGET_ROOTS[@]}")
MARKER="<!-- skillspector-pr-skills -->"
ROOTS_LIST="$(IFS=', '; echo "${TARGET_ROOTS[*]}")"
if [ "${#CHANGED_FILES[@]}" -eq 0 ]; then
echo "No added/updated files found under: ${ROOTS_LIST}."
{
echo "$MARKER"
echo "## SkillSpector PR Skills Scan"
echo ""
echo "No added/updated files found under: \`${ROOTS_LIST}\`."
} > skillspector-pr-reports/summary.md
exit 0
fi
declare -A SEEN_DIRS=()
SKILL_DIRS=()
for file in "${CHANGED_FILES[@]}"; do
for root in "${TARGET_ROOTS[@]}"; do
case "$file" in
"$root"/*)
rel="${file#${root}/}"
skill_name="${rel%%/*}"
if [ -z "$skill_name" ] || [ "$skill_name" = "$rel" ]; then
break
fi
dir="${root}/${skill_name}"
if [ -d "$dir" ] && [ -z "${SEEN_DIRS[$dir]+x}" ]; then
SEEN_DIRS[$dir]=1
SKILL_DIRS+=("$dir")
fi
break
;;
esac
done
done
if [ "${#SKILL_DIRS[@]}" -eq 0 ]; then
{
echo "$MARKER"
echo "## SkillSpector PR Skills Scan"
echo ""
echo "Changes were detected under \`${ROOTS_LIST}\`, but no skill directories were available to scan."
} > skillspector-pr-reports/summary.md
exit 0
fi
LLM_FLAG=""
LLM_MODE="enabled"
if [ -z "$OPENAI_API_KEY" ]; then
LLM_FLAG="--no-llm"
LLM_MODE="disabled (OPENAI_API_KEY not set)"
echo "LLM disabled (OPENAI_API_KEY not set)."
fi
HIGH_RISK_FOUND=0
{
echo "$MARKER"
echo "## SkillSpector PR Skills Scan"
echo ""
echo "- Base SHA: \`$BASE_SHA\`"
echo "- Head SHA: \`$HEAD_SHA\`"
echo "- LLM: $LLM_MODE"
echo ""
echo "Scanned directories:"
for dir in "${SKILL_DIRS[@]}"; do
echo "- \`${dir}\`"
done
echo ""
echo "---"
echo ""
} > skillspector-pr-reports/summary.md
for dir in "${SKILL_DIRS[@]}"; do
slug="$(echo "$dir" | tr '/.' '__')"
set +e
skillspector scan "$dir" $LLM_FLAG --format markdown --output "skillspector-pr-reports/${slug}.md"
MD_EXIT=$?
set -e
if [ "$MD_EXIT" -eq 2 ]; then
echo "Failed generating markdown report for ${dir}."
exit 2
fi
if [ "$MD_EXIT" -eq 1 ]; then
HIGH_RISK_FOUND=1
fi
{
echo "### \`${dir}\`"
echo ""
cat "skillspector-pr-reports/${slug}.md"
echo ""
echo "---"
echo ""
} >> skillspector-pr-reports/summary.md
done
if [ "$HIGH_RISK_FOUND" -eq 1 ]; then
{
echo "High-risk findings were detected in one or more changed skill directories."
echo "Review the report sections above before merging."
} >> skillspector-pr-reports/summary.md
else
echo "No DO_NOT_INSTALL outcome detected in changed skill directories." >> skillspector-pr-reports/summary.md
fi
COMMENT_MAX=65000
CURRENT_SIZE=$(wc -c < skillspector-pr-reports/summary.md | tr -d ' ')
if [ "$CURRENT_SIZE" -gt "$COMMENT_MAX" ]; then
{
echo ""
echo "> ⚠️ Report was truncated to fit GitHub PR comment limits."
} > skillspector-pr-reports/truncation-note.md
head -c "$COMMENT_MAX" skillspector-pr-reports/summary.md > skillspector-pr-reports/pr-comment.md
cat skillspector-pr-reports/truncation-note.md >> skillspector-pr-reports/pr-comment.md
else
cp skillspector-pr-reports/summary.md skillspector-pr-reports/pr-comment.md
fi
- name: Create or update PR comment
if: always()
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const fs = require("fs");
const marker = "<!-- skillspector-pr-skills -->";
const fallback = `${marker}\n## SkillSpector PR Skills Scan\n\nThe scan step failed before a full report could be generated. Check the workflow logs.`;
const body = fs.existsSync("skillspector-pr-reports/pr-comment.md")
? fs.readFileSync("skillspector-pr-reports/pr-comment.md", "utf8")
: fallback;
const issue_number = context.payload.pull_request.number;
const { owner, repo } = context.repo;
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100,
});
const existing = comments.find(c => c.user?.type === "Bot" && c.body?.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
});
}
- name: Print summary to job log
if: always()
run: |
if [ -f skillspector-pr-reports/pr-comment.md ]; then
cat skillspector-pr-reports/pr-comment.md >> "$GITHUB_STEP_SUMMARY"
else
echo "## SkillSpector PR Skills Scan" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Report was not generated. Check earlier step logs." >> "$GITHUB_STEP_SUMMARY"
fi

Wrapping Up
This pattern fits shared repositories of GitHub Copilot skills, internal engineering prompts, platform standards, Terraform guidance, API operation patterns, diagramming rules, and MCP configuration examples.
It is especially useful when skills are copied or reused across teams. Once people start trusting a skill, the repository holding it needs consistent checks and reviewable output.
I would be more cautious with blanket full-repository scanning everywhere. Some repositories are too large, too noisy, or contain content that should not be sent to an LLM provider. Start with targeted static scans if that is the safer path.
If a file can influence what an agent does, treat it as part of the engineering system. Scan it, review it, pin the tooling around it, and make the output visible where engineers already work. The control does not need to be clever. It needs to run every time.
GitHub repository containing all setup examples of the above (& includes a local setup as well)