Run Semgrep static analysis scan on a codebase using parallel subagents. Automatically detects and uses Semgrep Pro for cross-file analysis when available...
Run a Semgrep scan with automatic language detection, parallel execution, and merged SARIF output.
--metrics=off β Semgrep sends telemetry by default; --config auto also phones home. Every semgrep command must include --metrics=off to prevent data leakage during security audits.scripts/run-scans.sh generates the commands; do not write them yourself β it builds every semgrep line from the approved list. That is what makes --metrics=off, the --include scoping rule, and the parallel dispatch properties of the code rather than instructions. Give it the approved rulesets and let it run.scans.json carries failed and skipped alongside scans. A ruleset whose repo would not clone, or whose scan exited non-zero, must appear in the report. A partial scan presented as a complete one is worse than no scan.semgrep-rule-creator skillsemgrep-rule-variant-creator skillAll scan results, SARIF files, and temporary data are stored in a single output directory.
OUTPUT_DIR../static_analysis_semgrep_1. If that already exists, increment to _2, _3, etc.In both cases, always create the directory with mkdir -p before writing any files.
# Resolve output directory
if [ -n "$USER_SPECIFIED_DIR" ]; then
OUTPUT_DIR="$USER_SPECIFIED_DIR"
else
BASE="static_analysis_semgrep"
N=1
while [ -e "${BASE}_${N}" ]; do
N=$((N + 1))
done
OUTPUT_DIR="${BASE}_${N}"
fi
mkdir -p "$OUTPUT_DIR/raw" "$OUTPUT_DIR/results"
The output directory is resolved once at the start of Step 1 and used throughout all subsequent steps.
$OUTPUT_DIR/
βββ rulesets.json # The approved plan (Step 3), read by run-scans.sh (Step 4)
βββ scans.json # What ran, failed, skipped, and covered nothing (Step 4)
βββ raw/ # Per-scan raw output (unfiltered)
β βββ python-python.json # <language>-<ruleset> for language-scoped rules
β βββ python-python.sarif
β βββ python-django.json
β βββ python-django.sarif
β βββ all-security-audit.json # all-<ruleset> for cross-language rules, run once
β βββ all-security-audit.sarif
β βββ ...
βββ results/ # Final merged output
βββ results.sarif
Required: Semgrep CLI (semgrep --version). If not installed, see Semgrep installation docs.
Optional: Semgrep Pro β enables cross-file taint tracking, inter-procedural analysis, and additional languages (Apex, C#, Elixir). Check with:
# --metrics=off because Principle 1 has no exceptions, and this is the first semgrep command
# of a run. stderr is kept because "OSS only" has several causes (logged out, no subscription,
# registry blocked) and the run downgrades silently for all of them.
if PRO_ERR=$(semgrep --pro --validate --metrics=off --config p/default 2>&1); then
echo "Pro available"
else
echo "OSS only"
echo " reason: $(printf '%s' "$PRO_ERR" | tail -n 3)"
fi
Limitations: OSS mode cannot track data flow across files. Pro mode uses -j 1 for cross-file analysis (slower per ruleset, but parallel rulesets compensate).
Select mode in Step 2. Mode affects both the scan flags and post-processing.
| Mode | Coverage | Findings Reported |
|---|---|---|
| Run all | All rulesets, all severity levels | Everything |
| Important only | All rulesets, pre- and post-filtered | Security vulns only, medium-high confidence/impact |
Important only applies two filter layers:
--severity WARNING --severity ERROR (CLI flag)category=security, confidenceβ{MEDIUM,HIGH}, impactβ{MEDIUM,HIGH}See scan-modes.md for metadata criteria and jq filter commands.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MAIN SESSION (this skill) β
β Step 1: Detect languages + check Pro availability β
β Step 2: Select scan mode + rulesets (ref: rulesets.md) β
β Step 3: Present plan + rulesets, get approval [β HARD GATE] β
β Step 4: Run scripts/run-scans.sh with the approved rulesets β
β Step 5: Post-filter, merge, report, delete repos/ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 4: Bash
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β scripts/run-scans.sh β
β clone each third-party repo once, into repos/ β
β generate one semgrep command per ruleset β
β βββ python p/python, p/django --include=*.pyβ
β βββ javascript p/javascript --include=*.jsβ
β βββ docker p/dockerfile β
β βββ cross-language p/security-audit, p/secrets, β
β the cloned repos (no filter) β
β run in batches of --jobs, exit code read per process β
β write scans.json β scans, failed, skipped β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The approval gate stays in the session; the script is execution only and asks nothing. The approved list reaches it as a JSON file, so the scan cannot reach a ruleset the user declined.
Cross-language rulesets go in one shared unit rather than being repeated per language.
p/security-audit, p/secrets, and the third-party repos scan the whole target unscoped,
so running them once per language ran the identical command N times and left the SARIF
merge to dedup the copies.
This plugin ships /static-analysis:semgrep-scan, which runs the whole scan end to end:
detect languages and Pro, select rulesets from rulesets.md, run
scripts/run-scans.sh, merge and report. Pass it a JSON object, not prose:
/static-analysis:semgrep-scan {"target": "/abs/path", "mode": "run-all"}
It does not stop for ruleset approval. Invoking it with a target is the opt-in, the same
way /variant-analysis:variants works. That is safe to do because the scan is read-only over
the target β no --autofix, every write inside the output directory β so the approval gate
below is a scope confirmation rather than a safety one. What ran is recorded in
rulesets.json and scans.json either way.
Use the workflow when you want the scan run; work the five steps below when the ruleset selection itself matters and you want to see and edit the list first.
Follow the detailed workflow in scan-workflow.md. Summary:
| Step | Action | Gate | Key Reference |
|---|---|---|---|
| 1 | Resolve output dir, detect languages + Pro availability | β | Use Glob, not Bash |
| 2 | Select scan mode + rulesets | β | rulesets.md |
| 3 | Present plan, get explicit approval | β HARD | AskUserQuestion |
| 4 | Run the scans | β | scripts/run-scans.sh |
| 5 | Post-filter, merge, report, clean up | β | Merge script (below) |
Task enforcement: On invocation, create 5 tasks with blockedBy dependencies (each step blocks the previous). Step 3 is a HARD GATE β mark complete ONLY after user explicitly approves.
Merge command (Step 5):
# run-all
uv run --no-project {baseDir}/scripts/merge_sarif.py "$OUTPUT_DIR/raw" "$OUTPUT_DIR/results/results.sarif" \
--scans "$OUTPUT_DIR/scans.json"
# important-only, once the JSON post-filter has run over every file in raw/
uv run --no-project {baseDir}/scripts/merge_sarif.py "$OUTPUT_DIR/raw" "$OUTPUT_DIR/results/results.sarif" \
--important --scans "$OUTPUT_DIR/scans.json"
--scans drops the output of scans listed under .failed. A scan that died part-way may still
have written a .sarif, and under --important that file has no post-filter beside it, which is
an error rather than an empty filter. Without the flag one dead scan denies every healthy scan a
merged result. The excluded files are named on stdout, so they can go in the report.
The post-filter reads metadata SARIF does not carry, so it cannot be re-run against the merged
file; --important instead keeps the findings the JSON filter kept, matched on
(rule, file, line). Without it results.sarif is unfiltered while the JSON side is not.
| Component | Purpose |
|---|---|
scripts/run-scans.sh |
Builds every scan command from the approved rulesets, runs them in batches, and writes scans.json |
Step 4 is a Bash call. No subagent runs any part of the scan: exit codes and finding counts are read from the processes and the JSON they wrote.
| Shortcut | Why It's Wrong |
|---|---|
| "User asked for scan, that's approval" | Original request β plan approval. Present plan, use AskUserQuestion, await explicit "yes" |
| "Step 3 task is blocking, just mark complete" | Lying about task status defeats enforcement. Only mark complete after real approval |
| "I already know what they want" | Assumptions cause scanning wrong directories/rulesets. Present plan for verification |
| "Just use default rulesets" | User must see and approve exact rulesets before scan |
| "Add extra rulesets without asking" | Modifying approved list without consent breaks trust |
| "Third-party rulesets are optional" | Trail of Bits, 0xdea, Decurity catch vulnerabilities not in official registry β REQUIRED |
| "Use --config auto" | Sends metrics; less control over rulesets |
| "I'll just run the semgrep commands myself" | run-scans.sh is what enforces --metrics=off, the --include rule and the output-directory --exclude. Hand-written commands drop them silently |
| "The script failed, I'll run semgrep directly to get something" | A non-zero exit means no scan succeeded. Report that and stop; a hand-run subset reads as a full scan |
| "Some scans failed, the run still finished" | failed and skipped are part of scans.json. Report them or the user reads a partial scan as a clean one |
| "Pro is too slow, skip --pro" | Cross-file analysis catches 250% more true positives; worth the time |
| "Semgrep handles GitHub URLs natively" | URL handling fails on repos with non-standard YAML; always clone first |
| "Cleanup is optional" | Cloned repos pollute the user's workspace and accumulate across runs |
"Use . or relative path as target" |
Subagents need absolute paths to avoid ambiguity |
| "Let the user pick an output dir later" | Output directory must be resolved at Step 1, before any files are created |
| File | Content |
|---|---|
| rulesets.md | Complete ruleset catalog and selection algorithm |
| scan-modes.md | Pre/post-filter criteria and jq commands |
| Workflow | Purpose |
|---|---|
| scan-workflow.md | Complete 5-step scan execution process |
scripts/run-scans.sh |
The scan runner Step 4 calls |
$OUTPUT_DIRrun-scans.sh exited 0 and wrote $OUTPUT_DIR/scans.jsonfailed and skipped from scans.json are empty, or listed in the reportpartial in scans.json are none, or listed in the report β they ran with some of their rules failing to compilesemgrep command used --metrics=off$OUTPUT_DIR/rulesets.json at the Step 3 gate, and passed to
the scanner unchangedcoveredNothing from scans.json is empty, or listed in the reportoversized from scans.json is empty, or listed in the report β those files exceed
--max-target-bytes and no ruleset opened them$OUTPUT_DIR/raw/results.sarif exists in $OUTPUT_DIR/results/ and is valid JSON--important, unfiltered results preserved in raw/$OUTPUT_DIR/repos/