Generated banner for Semgrep
Generated logo for Semgrep

Semgrep

Trail of BitsOfficialPublished by Trail of Bits

  • Skill
  • Workflow
  • Security
  • CC-BY-SA-4.0
  • Low risk
  • Has scripts

From the author

Runs a Semgrep security scan over a codebase: detects languages, selects rulesets, presents the plan for explicit approval, then runs every approved ruleset through scripts/run-scans.sh, which batches the semgrep processes and writes scans.json, and merges the output to SARIF. Supports two scan modes, "run all" for full ruleset coverage and "important only" for security findings at medium-to-high confidence and impact. Uses Semgrep Pro for cross-file taint analysis when it is available. Use when asked to scan code for vulnerabilities, run a security audit with Semgrep, find bugs, or perform static analysis. For the same scan without the approval gate, use the /static-analysis:semgrep-scan workflow.
— the description by Trail of Bits

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill automates the execution of Semgrep security scans across a codebase. It manages the entire lifecycle of a scan: detecting languages, selecting appropriate rulesets, requiring explicit user approval for the scan plan, executing scans in parallel batches, and merging results into a unified SARIF file. It supports two modes: a full scan of all rulesets and an important-only mode that filters findings based on security category, confidence, and impact metadata.

What it does

  • Detects programming languages and checks for Semgrep Pro availability.
  • Selects relevant official and third-party rulesets based on detected languages.
  • Presents a detailed scan plan for explicit user approval before execution.
  • Executes scans using batched processes to generate raw JSON and SARIF outputs.
  • Applies optional post-filtering to focus on high-confidence security vulnerabilities.
  • Merges all scan results into a single, consolidated SARIF report.

When to use it

  • Performing a security audit of a codebase.
  • Identifying potential vulnerabilities prior to code review.
  • Scanning for known bug patterns.
  • Conducting first-pass static analysis.

Advantages

  • Enforces security best practices like disabling telemetry during scans.
  • Ensures consistent scan configuration through automated script execution.
  • Provides a clear approval gate to confirm scan scope and rulesets.
  • Supports cross-file taint analysis when Semgrep Pro is available.
  • Consolidates findings from multiple rulesets into a single report.

Usage examples

“Run a security scan on the current directory.”
The agent detects the languages, proposes a ruleset plan, and waits for your approval before running the scan.
“Perform an important-only security audit on /path/to/code.”
The agent configures the scan to filter for medium-to-high confidence security findings and generates a merged SARIF report.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/trailofbits/semgrep/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip semgrep.zip -d .agents/skills

Documentation by Trail of Bits

Semgrep Security Scan

Run a Semgrep scan with automatic language detection, parallel execution, and merged SARIF output.

Essential Principles

  1. Always use --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.

  2. User must approve the scan plan (Step 3 is a hard gate) — The original "scan this codebase" request is NOT approval. Present exact rulesets, target, engine, and mode; wait for explicit "yes"/"proceed" before spawning scanners.

  3. Third-party rulesets are required, not optional — Trail of Bits, 0xdea, and Decurity rules catch vulnerabilities absent from the official registry. Include them whenever the detected language matches.

  4. 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.

  5. Always check for Semgrep Pro before scanning — Pro enables cross-file taint tracking and catches ~250% more true positives. Skipping the check means silently missing critical inter-file vulnerabilities.

  6. Report what did not 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.

When to Use

  • Security audit of a codebase

  • Finding vulnerabilities before code review

  • Scanning for known bug patterns

  • First-pass static analysis

When NOT to Use

  • Binary analysis → Use binary analysis tools

  • Already have Semgrep CI configured → Use existing pipeline

  • Need cross-file analysis but no Pro license → Consider CodeQL as alternative

  • Creating custom Semgrep rules → Use semgrep-rule-creator skill

  • Porting existing rules to other languages → Use semgrep-rule-variant-creator skill

Output Directory

All scan results, SARIF files, and temporary data are stored in a single output directory.

  • If the user specifies an output directory in their prompt, use it as OUTPUT_DIR.

  • If not specified, default to ./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

Prerequisites

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).

Scan Modes

Select mode in Step 2. Mode affects both the scan flags and post-processing.

ModeCoverageFindings Reported
Run allAll rulesets, all severity levelsEverything
Important onlyAll rulesets, pre- and post-filteredSecurity vulns only, medium-high confidence/impact

Important only applies two filter layers:

  1. Pre-filter: --severity WARNING --severity ERROR (CLI flag)

  2. Post-filter: JSON metadata — keeps only category=security, confidence∈{MEDIUM,HIGH}, impact∈{MEDIUM,HIGH}

See scan-modes.md for metadata criteria and jq filter commands.

Orchestration Architecture

┌──────────────────────────────────────────────────────────────────┐
│ 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.

Running it as a Workflow

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.

Workflow

Follow the detailed workflow in scan-workflow.md. Summary:

StepActionGateKey Reference
1Resolve output dir, detect languages + Pro availability—Use Glob, not Bash
2Select scan mode + rulesets—rulesets.md
3Present plan, get explicit approval⛔ HARDAskUserQuestion
4Run the scans—scripts/run-scans.sh
5Post-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.

Workflow and agents

ComponentPurpose
scripts/run-scans.shBuilds 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.

Rationalizations to Reject

ShortcutWhy 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

Reference Index

FileContent
rulesets.mdComplete ruleset catalog and selection algorithm
scan-modes.mdPre/post-filter criteria and jq commands
WorkflowPurpose
scan-workflow.mdComplete 5-step scan execution process
scripts/run-scans.shThe scan runner Step 4 calls

Success Criteria

  • [ ] Output directory resolved (user-specified or auto-incremented default)

  • [ ] All generated files stored inside $OUTPUT_DIR

  • [ ] Languages detected with file counts; Pro status checked

  • [ ] Scan mode selected by user (run all / important only)

  • [ ] Rulesets include third-party rules for all detected languages

  • [ ] User explicitly approved the scan plan (Step 3 gate passed)

  • [ ] run-scans.sh exited 0 and wrote $OUTPUT_DIR/scans.json

  • [ ] failed and skipped from scans.json are empty, or listed in the report

  • [ ] Scans marked partial in scans.json are none, or listed in the report — they ran with some of their rules failing to compile

  • [ ] Every semgrep command used --metrics=off

  • [ ] Approved plan written to $OUTPUT_DIR/rulesets.json at the Step 3 gate, and passed to the scanner unchanged

  • [ ] coveredNothing from scans.json is empty, or listed in the report

  • [ ] oversized from scans.json is empty, or listed in the report — those files exceed --max-target-bytes and no ruleset opened them

  • [ ] Raw per-scan outputs stored in $OUTPUT_DIR/raw/

  • [ ] results.sarif exists in $OUTPUT_DIR/results/ and is valid JSON

  • [ ] Important-only mode: post-filter applied before merge, merge run with --important, unfiltered results preserved in raw/

  • [ ] Results summary reported with severity and category breakdown

  • [ ] Cloned repos (if any) cleaned up from $OUTPUT_DIR/repos/

Files

workflows/scan-workflow.md

Semgrep Scan Workflow

Complete 5-step scan execution process. Read from start to finish and follow each step in order.

Task System Enforcement

On invocation, create these tasks with dependencies:

TaskCreate: "Detect languages and Pro availability" (Step 1)
TaskCreate: "Select scan mode and rulesets" (Step 2) - blockedBy: Step 1
TaskCreate: "Present plan with rulesets, get approval" (Step 3) - blockedBy: Step 2
TaskCreate: "Execute scans with approved rulesets and mode" (Step 4) - blockedBy: Step 3
TaskCreate: "Merge results and report" (Step 5) - blockedBy: Step 4
Mandatory Gate
TaskGate TypeCannot Proceed Until
Step 3HARD GATEUser explicitly approves rulesets + plan

Mark Step 3 as completed ONLY after user says "yes", "proceed", "approved", or equivalent.


Step 1: Resolve Output Directory, Detect Languages and Pro Availability

Entry: User has specified or confirmed the target directory. Exit: OUTPUT_DIR resolved and created; language list with file counts produced; Pro availability determined.

Resolve Output Directory

If the user specified an output directory in their prompt, use it as OUTPUT_DIR. Otherwise, auto-increment. In both cases, always mkdir -p to ensure the directory exists.

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"

# Absolute from here on. run-scans.sh rejects a relative path, and that rejection lands
# *after* the user has passed the hard gate, so a path this skill generated itself would send
# them back through approval.
OUTPUT_DIR=$(cd "$OUTPUT_DIR" && pwd)
# The -d test first: `cd ""` returns 0, so a TARGET that was never bound would pass a bare
# `cd || exit` and silently resolve to the session's CWD, scanning whatever happens to be there.
[ -n "$TARGET" ] && [ -d "$TARGET" ] || { echo "ERROR: TARGET is unset or not a directory"; exit 1; }
TARGET=$(cd "$TARGET" && pwd)
echo "Output directory: $OUTPUT_DIR"
echo "Target: $TARGET"

Pass $TARGET and $OUTPUT_DIR to Step 4 exactly as resolved here. Do not re-derive either.

$OUTPUT_DIR is used by all subsequent steps. Raw per-scan output goes to $OUTPUT_DIR/raw/; merged and filtered results go to $OUTPUT_DIR/results/.

Detect Pro availability (requires Bash):

if ! command -v semgrep >/dev/null 2>&1; then
  echo "ERROR: semgrep is not installed. Install from https://semgrep.dev/docs/getting-started/"
  exit 1
fi
semgrep --version
# --metrics=off applies here too. This is the first semgrep invocation of the run and it
# resolves p/default against the registry, so without the flag an audit phones home before
# the user has approved anything. Principle 1 has no exceptions.
semgrep --pro --validate --metrics=off --config p/default 2>/dev/null && echo "Pro: AVAILABLE" || echo "Pro: NOT AVAILABLE"

Detect languages using Glob (not Bash). Run these patterns against the target directory and count matches:

**/*.py, **/*.pyi, **/*.js, **/*.jsx, **/*.mjs, **/*.cjs, **/*.ts, **/*.tsx, **/*.go, **/*.rb, **/*.java, **/*.jsp, **/*.kt, **/*.kts, **/*.php, **/*.phtml, **/*.c, **/*.cc, **/*.cpp, **/*.cxx, **/*.h, **/*.hh, **/*.hpp, **/*.hxx, **/*.cs, **/*.rs, **/*.scala, **/*.swift, **/*.ex, **/*.exs, **/*.cls, **/*.trigger, **/*.sol, **/Dockerfile, **/*.dockerfile, **/*.tf, **/*.tfvars, **/*.hcl, **/*.yaml, **/*.yml, **/*.json

Step 2 can only select a ruleset for a category this step detected, so an extension missing here removes its ruleset from the scan with no signal — the report then reads clean rather than incomplete. The list is the union of the includes_for globs in run-scans.sh; keep the two in sync when either changes. .mts, .cts, .C, Containerfile, and Dockerfile.prod are absent from both, because semgrep does not parse them.

Two extensions are matched by glob but assigned by content, not by extension. .yaml/.yml and .json each feed several categories, and both are common in repositories that have no infrastructure to scan at all — nearly every project carries package.json, tsconfig.json and a lockfile. Assigning a category from the extension alone would attach an AWS IAM ruleset to every scan and report a JSON "language" for a project that has none. Assigning nothing would leave r/json.aws and JSON-format CloudFormation unreachable, which is worse: an unselected category never enters rulesets.json, so it cannot appear in coveredNothing, failed or skipped either, and the report reads clean. Glob for both, then read a sample and assign on the markers below.

Also check for framework markers: **/package.json, **/pyproject.toml, **/requirements.txt, **/Gemfile, **/composer.json, **/go.mod, **/Cargo.toml, **/pom.xml. Use Read to inspect these files for framework dependencies (e.g., read package.json to detect React, Express, Next.js; read pyproject.toml for Django, Flask, FastAPI). The **/ prefix is required, not cosmetic: a bare package.json matches only the target root, so a monorepo with packages/*/package.json or services/*/go.mod gets no framework rulesets at all.

Map findings to categories:

DetectionCategory
.py, .pyi, pyproject.toml, requirements.txtPython
.js, .jsx, .mjs, .cjs, .ts, .tsx, package.jsonJavaScript/TypeScript
.go, go.modGo
.rb, GemfileRuby
.java, .jsp, pom.xmlJava
.kt, .ktsKotlin
.php, .phtml, composer.jsonPHP
.c, .cc, .cpp, .cxx, .h, .hh, .hpp, .hxxC/C++
.csC#
.rs, Cargo.tomlRust
.scalaScala
.swiftSwift
.ex, .exsElixir
.cls, .triggerApex
.solSolidity
Dockerfile, .dockerfileDocker
.tf, .tfvars, .hclTerraform
.yaml, .ymlYAML, Kubernetes, GitHub Actions, or CloudFormation — disambiguate below
.jsonCloudFormation or JSON, or no category at all — disambiguate below

Disambiguating YAML. One .yaml/.yml match feeds four categories, so Read a sample of the matches before assigning:

  • path under .github/workflows/ → GitHub Actions

  • apiVersion: together with kind: → Kubernetes

  • AWSTemplateFormatVersion:, or Resources: with a Type: AWS:: member → CloudFormation

  • anything else → YAML

These are not exclusive; assign every category that matches. Include the generic YAML category whenever any YAML is present, since p/yaml carries patterns the specific rulesets do not.

Disambiguating JSON. Unlike YAML, .json has no catch-all: most JSON in a repository is build configuration that no ruleset covers, so the default is to assign nothing. Read a sample and assign only on these markers:

  • "AWSTemplateFormatVersion", or "Resources" whose members carry a "Type": "AWS::…" → CloudFormation

  • a "Statement" array whose elements have "Effect" → JSON (this is the IAM policy shape r/json.aws targets)

  • anything else, including package.json, tsconfig.json, composer.json, lockfiles and editor settings → no category

Do not report a json category because JSON files exist. Report it when a sampled file has the IAM policy shape. Prefer sampling files whose path suggests infrastructure — iam/, policies/, cloudformation/, infra/, *template*.json — since a repository with thousands of JSON files will have its IAM policies outnumbered by build configuration, and a sample drawn without regard to path is likely to miss them.


Step 2: Select Scan Mode and Rulesets

Entry: Step 1 complete — languages detected, Pro status known. Exit: Scan mode selected; structured rulesets JSON compiled for all detected languages.

First, select scan mode using AskUserQuestion:

header: "Scan Mode"
question: "Which scan mode should be used?"
multiSelect: false
options:
  - label: "Run all (Recommended)"
    description: "Full coverage — all rulesets, all severity levels"
  - label: "Important only"
    description: "Security vulnerabilities only — medium-high confidence and impact, no code quality"

Record the selected mode. It affects Steps 4 and 5.

Then, select rulesets. Using the detected languages and frameworks from Step 1, follow the Ruleset Selection Algorithm in rulesets.md.

The algorithm covers:

  1. Security baseline (always included)

  2. Language-specific rulesets

  3. Framework rulesets (if detected)

  4. Infrastructure rulesets

  5. Required third-party rulesets (Trail of Bits, 0xdea, Decurity — NOT optional)

  6. Registry verification

Output: Structured JSON passed to Step 3 for user review:

{
  "baseline": ["p/security-audit", "p/secrets"],
  "python": ["p/python", "p/django"],
  "javascript": ["p/javascript", "p/react", "p/nodejs"],
  "docker": ["p/dockerfile"],
  "third_party": ["https://github.com/trailofbits/semgrep-rules"]
}

Step 3: CRITICAL GATE — Present Plan and Get Approval

Entry: Step 2 complete — scan mode and rulesets selected. Exit: User has explicitly approved the plan (quoted confirmation).

⛔ MANDATORY CHECKPOINT — DO NOT SKIP

This step requires explicit user approval before proceeding. User may modify rulesets before approving.

Present plan to user with explicit ruleset listing:

## Semgrep Scan Plan

**Target:** /path/to/codebase
**Output directory:** $OUTPUT_DIR
**Engine:** Semgrep Pro (cross-file analysis) | Semgrep OSS (single-file)
**Scan mode:** Run all | Important only (security vulns, medium-high confidence/impact)
[in important-only mode, add:] Note: important-only passes --severity WARNING --severity ERROR
to every command, including the third-party repos. Trail of Bits / 0xdea / Decurity rules that
ship with CLI severity INFO are dropped at scan time, before the metadata filter that would
otherwise keep them. Choose "Run all" if you want those.

### Detected Languages/Technologies:
- Python (1,234 files) - Django framework detected
- JavaScript (567 files) - React detected
- Dockerfile (3 files)

### Rulesets to Run:

**Security Baseline (always included):**
- [x] `p/security-audit` - Comprehensive security rules
- [x] `p/secrets` - Hardcoded credentials, API keys

**Python (1,234 files):**
- [x] `p/python` - Python security patterns
- [x] `p/django` - Django-specific vulnerabilities

**JavaScript (567 files):**
- [x] `p/javascript` - JavaScript security patterns
- [x] `p/react` - React-specific issues
- [x] `p/nodejs` - Node.js server-side patterns

**Docker (3 files):**
- [x] `p/dockerfile` - Dockerfile best practices

**Third-party (auto-included for detected languages):**
- [x] Trail of Bits rules - https://github.com/trailofbits/semgrep-rules

**Want to modify rulesets?** Tell me which to add or remove.
**Ready to scan?** Say "proceed" or "yes".

⛔ STOP: Await explicit user approval.

  1. If user wants to modify rulesets: Add/remove as requested, re-present the updated plan, return to waiting.

  2. Use AskUserQuestion if user hasn't responded:

    "I've prepared the scan plan with N rulesets (including Trail of Bits). Proceed with scanning?"
    Options: ["Yes, run scan", "Modify rulesets first"]
  3. Valid approval: "yes", "proceed", "approved", "go ahead", "looks good", "run it"

  4. NOT approval: User's original request ("scan this codebase"), silence, questions about the plan

Pre-Scan Checklist

Before marking Step 3 complete:

  • [ ] Target directory shown to user

  • [ ] Engine type (Pro/OSS) displayed

  • [ ] Languages detected and listed

  • [ ] All rulesets explicitly listed with checkboxes

  • [ ] User given opportunity to modify rulesets

  • [ ] User explicitly approved (quote their confirmation)

  • [ ] Final ruleset list captured for Step 4

Log Approved Rulesets

After approval, write the approved plan to $OUTPUT_DIR/rulesets.json. This is the same file Step 4 hands to the scanner: what the user approved and what runs are one artifact, so there is no second copy to transcribe and no way for the two to disagree.

Fill in the plan that was just approved. Every value is an array, even a single ruleset:

cat > "$OUTPUT_DIR/rulesets.json" << 'RULESETS'
{
  "baseline": [<the always-on rulesets from Step 2>],
  "<each detected language>": [<its approved rulesets>],
  "third_party": [<approved repository URLs>]
}
RULESETS

One key per language detected in Step 1, using the lowercase names from that step. A language key for a language the target does not contain scans nothing: its --include globs match no file, semgrep exits 0 with an empty result, and the report shows the ruleset with 0 findings exactly as it would for a ruleset that ran and found nothing. The script counts the files each scan opened and lists any that covered nothing under coveredNothing in scans.json, but getting the languages right here is what stops it happening.

Repository URLs go under third_party and nowhere else. Registry identifiers like p/python go under a language key; a https://… there fails the identifier check and the script exits without scanning.


Step 4: Run the Scans

Entry: Step 3 approved — user explicitly confirmed the plan. Exit: $OUTPUT_DIR/scans.json exists; result files exist in $OUTPUT_DIR/raw/.

Run the script against the plan Step 3 already wrote. One Bash call; there is no subagent in this step, and no second copy of the ruleset list to compose here.

{baseDir}/scripts/run-scans.sh \
  --target "$TARGET" \
  --output-dir "$OUTPUT_DIR" \
  --mode run-all \
  --rulesets "$OUTPUT_DIR/rulesets.json"

Do not rewrite rulesets.json here. It is the plan the user approved at the Step 3 gate, and regenerating it at this point is how a ruleset nobody agreed to reaches the scanner. If it needs to change, go back to Step 3 and get the change approved.

--mode is run-all or important-only. Add --pro only when Step 1 printed Pro: AVAILABLE; it puts --pro on every command, so passing it without a licence fails every scan in the run. --jobs N sets how many semgrep processes run at once (default 4); semgrep holds the rules and the scanned ASTs in memory, so raising it on a large tree trades memory for wall-clock.

Repository URLs go under third_party and nowhere else. A https://… under a language key fails the registry-identifier check and the script exits without scanning.

The script clones each third-party repo once, generates every semgrep command, and runs them in batches. --metrics=off, the --include scoping rule, --exclude for the output directory and the severity flags are all its job, not yours. It writes $OUTPUT_DIR/scans.json:

FieldMeaning
scansRulesets that ran, with json, sarif, findings, filesScanned, partial and exitCode for each. findings is counted from the JSON the scan wrote; filesScanned is how many files semgrep opened, or -1 when it did not say; exitCode is what semgrep exited with
scans[].partialtrue when the scan wrote complete output while some of its rules failed to compile — semgrep exits 2 and reports the rest of the run normally. The findings are real and in the merge; the rules that never compiled found nothing and cannot say so, so this reads as an unqualified success unless it is called out. Must be shown.
coveredNothingRulesets that ran against zero files, because their --include globs matched nothing in the target. They report 0 findings exactly like a ruleset that ran and found nothing, so a plan naming a language the target does not contain reads as a clean audit. Must be shown.
failedRulesets that ran and did not produce usable output, with the json and sarif paths they may have partly written, and the stderr excerpt. Must be shown to the user.
skippedRulesets dropped before scanning, mostly repos that would not clone. Must be shown.
unscopedLanguages with no --include globs, which ran against every file
alsoSharedRulesets dropped from a language because the same ruleset is already running unscoped over the whole target. Coverage is unaffected; report them so a per-ruleset accounting adds up
excludePatternSet when the output directory sits inside the target: the pattern passed as --exclude to every scan, or "". semgrep matches it anywhere in the tree, so out also drops src/out/. Must be shown when non-empty.
maxTargetBytesThe --max-target-bytes every scan ran with. semgrep opens no file larger than this
oversizedFiles an --include glob selected that are larger than maxTargetBytes. semgrep skipped them with no paths.skipped entry and no error, so they report 0 findings exactly like a file that was opened and found clean. A scan that did not open a file has not cleared it. Must be shown.
reposPathThe clone directory Step 5 deletes

A non-zero exit means no scan succeeded. The script exits 1 when scans is empty, so a run that produced nothing fails loudly rather than handing Step 5 an empty result to report as zero findings. Read the message, say that no scan ran, and stop; do not retry with adjusted arguments, because the approved plan is what produced them.

If failed or skipped is non-empty, carry both into the Step 5 report. A run that covered four of nine rulesets reads exactly like one that covered four of four unless you say otherwise. The same line is why any scan with partial: true is carried across as well: it is in scans as a success, so the rules of it that never ran are invisible in every count the report otherwise prints.


Step 5: Merge Results and Report

Entry: Step 4 complete — the workflow returned. Exit: results.sarif exists in $OUTPUT_DIR/results/ and is valid JSON; repos/ deleted.

Read the result with jq from $OUTPUT_DIR/scans.json. Every entry there was written after the script checked the exit code and confirmed both output files were non-empty, so the entries do not need re-verifying.

Important-only mode: Post-filter before merge. Apply the filter from scan-modes.md ("Filter All Result Files in a Directory" section) to each result JSON in $OUTPUT_DIR/raw/. The filter creates *-important.json files alongside the originals — the originals are preserved unmodified.

Generate merged SARIF using the merge script. The resolved path is in SKILL.md's "Merge command" section — use that exact path:

# 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 post-filter above 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"
  • Run-all mode: The script merges all *.sarif files from $OUTPUT_DIR/raw/.

  • Important-only mode: --important is not optional. The JSON post-filter does not touch the SARIF files the merge reads, so without that flag results.sarif keeps every finding the mode exists to exclude while the JSON side is correctly filtered, and the Total findings counted from it is the run-all total.

    Do not try to run the jq filter from scan-modes.md against a .sarif file. It reads .results[].extra.metadata, which SARIF does not have — there is no top-level .results at all — so it exits with Cannot iterate over null and, if redirected over its own input, truncates the merged SARIF to nothing. --important matches findings across the two formats on (rule, file, line), the same key the merge dedups on, and fails rather than filtering if any scan in raw/ has no *-important.json beside it.

Verify merged SARIF is valid:

python -c "import json; d=json.load(open('$OUTPUT_DIR/results/results.sarif')); print(f'{sum(len(r.get(\"results\",[]))for r in d.get(\"runs\",[]))} findings in merged SARIF')"

If verification fails, the merge script produced invalid output — investigate before reporting.

Delete the cloned rulesets once the merge has succeeded. The workflow clones each third-party repo into repos/ and leaves it there for the scanners; this is the only place the deletion happens, and nothing that reads it is still running by now.

[ -n "$OUTPUT_DIR" ] && rm -rf "$OUTPUT_DIR/repos"

Report to user:

## Semgrep Scan Complete

**Scanned:** 1,804 files
**Rulesets used:** 9 (including Trail of Bits)
**Total findings:** 156   [count this from results.sarif, never by summing scans[].findings:
one finding flagged by two rulesets is one row in the merge and two in that sum]

### By Severity:
- ERROR: 5
- WARNING: 18
- INFO: 9

### By Category:
- SQL Injection: 3
- XSS: 7
- Hardcoded secrets: 2
- Insecure configuration: 12
- Code quality: 8

### Did Not Run:
[omit this section only when failed and skipped are both empty]
- Skipped: <ruleset> — <reason from the workflow>
- Failed: <ruleset> — <error from the workflow>

### Ran Partially:
[omit when no scan has partial: true]
- <ruleset> — ran and wrote full output, but some of its rules failed to compile (semgrep exit
  <exitCode>). Its findings are in the total below; the rules that did not compile scanned
  nothing, so this ruleset's coverage is narrower than its entry in the scan count suggests

### Also Covered Unscoped:
[omit when alsoShared is empty]
- <ruleset> — already running over the whole target from the baseline, so it was not scanned
  again under <language>. Coverage is unaffected; this is why the ruleset count and the scan
  count differ

### Ran Unscoped:
[omit when unscoped is empty]
- <language> — no --include map, so its rulesets ran against every file

### Covered Nothing:
[omit when coveredNothing is empty]
- <language>/<ruleset> — matched no file in the target, so it reports 0 findings without having
  looked at anything. Check the plan against the languages Step 1 detected: this is what a
  ruleset for a language the target does not contain looks like

### Too Large To Scan:
[omit when oversized is empty]
- <path> — larger than maxTargetBytes, so semgrep never opened it and no ruleset covered it.
  Not a clean result: re-run with a higher --max-target-bytes, or say in the report that the
  file was left unscanned

### Missing From The Merge:
[omit when the merge printed no "unparseable:" line]
- <file> — the scan succeeded and is counted in scans.json, but its SARIF could not be parsed,
  so its findings are not in results.sarif. The total below is short by that scan's `findings`
  count from scans.json

### Excluded From Every Scan:
[omit when excludePattern is empty]
- <excludePattern> — the output directory sits inside the target, so this pattern was excluded
  from every scan. semgrep matches it anywhere in the tree, so any other directory with that
  name was skipped too. Move the output directory outside the target to scan those files

### Default Ignore:
[one cheap check, no per-file work: is there a `.semgrepignore` in the target root? If yes, the
 project chose its own exclusions deliberately — omit this section. If no, semgrep's built-in
 default governed what was skipped — show the line below. semgrep records nothing about these
 drops (paths.skipped is empty), so there is no count to compute.]
- No repo `.semgrepignore`, so semgrep's built-in default (plus `.gitignore`) governed exclusions:
  vendored, minified, dist, build, and test paths were skipped, so they are not in the scanned
  count and not a clean result for those paths. If findings look low or you suspect a path was
  missed, re-run against it or add a repo `.semgrepignore` to take control.

Results written to:
- $OUTPUT_DIR/results/results.sarif (merged SARIF)
- $OUTPUT_DIR/raw/ (per-scan raw results, unfiltered)
- $OUTPUT_DIR/rulesets.json (the approved plan, as passed to the scanner)

Verify before reporting: confirm results.sarif exists and is valid JSON.

Requirements and compatibility

Written for: Claude Code

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Declares allowed tools, Names Claude Code tools, Needs Python

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Trail of Bits
Publisher
Trail of Bits (Official)
Source
trailofbits/skills · plugins/static-analysis/skills/semgrep · commit e4dd0bb
Licence
Creative Commons Attribution-ShareAlike 4.0 International (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Trail of Bits.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download semgrep.zip