Semgrep
Trail of BitsOfficialPublished by Trail of Bits
- Skill
- Workflow
- Security
- CC-BY-SA-4.0
- Low risk
- Has scripts
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
Download the skill as a ZIP:
https://autogineer.com/agent-skills/trailofbits/semgrep/downloadUnzip 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
Always use
--metrics=off— Semgrep sends telemetry by default;--config autoalso phones home. Everysemgrepcommand must include--metrics=offto prevent data leakage during security audits.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.
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.
scripts/run-scans.shgenerates the commands; do not write them yourself — it builds everysemgrepline from the approved list. That is what makes--metrics=off, the--includescoping rule, and the parallel dispatch properties of the code rather than instructions. Give it the approved rulesets and let it run.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.
Report what did not run —
scans.jsoncarriesfailedandskippedalongsidescans. 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-creatorskillPorting existing rules to other languages → Use
semgrep-rule-variant-creatorskill
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.sarifPrerequisites
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)"
fiLimitations: 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.
| 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:
Pre-filter:
--severity WARNING --severity ERROR(CLI flag)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:
| 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.
Workflow and agents
| 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.
Rationalizations to Reject
| 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 |
Reference Index
| 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 |
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.shexited 0 and wrote$OUTPUT_DIR/scans.json[ ]
failedandskippedfromscans.jsonare empty, or listed in the report[ ] Scans marked
partialinscans.jsonare none, or listed in the report — they ran with some of their rules failing to compile[ ] Every
semgrepcommand used--metrics=off[ ] Approved plan written to
$OUTPUT_DIR/rulesets.jsonat the Step 3 gate, and passed to the scanner unchanged[ ]
coveredNothingfromscans.jsonis empty, or listed in the report[ ]
oversizedfromscans.jsonis empty, or listed in the report — those files exceed--max-target-bytesand no ruleset opened them[ ] Raw per-scan outputs stored in
$OUTPUT_DIR/raw/[ ]
results.sarifexists in$OUTPUT_DIR/results/and is valid JSON[ ] Important-only mode: post-filter applied before merge, merge run with
--important, unfiltered results preserved inraw/[ ] Results summary reported with severity and category breakdown
[ ] Cloned repos (if any) cleaned up from
$OUTPUT_DIR/repos/
Files
- ATTRIBUTION.mdAdded by AutoGineer923 B
- LICENSELicence file added by AutoGineer19.7 KiB
- SKILL.md15.3 KiB
- agents/openai.yaml229 B
- assets/trail-of-bits-mark.svg3.0 KiB
- references/rulesets.md7.5 KiB
- references/scan-modes.md8.5 KiB
- scripts/merge_sarif.pyScript13.7 KiB
- scripts/run-scans.shScript33.6 KiB
- scripts/test_merge_sarif.pyScript22.4 KiB
- workflows/scan-workflow.md24.1 KiB
scripts/merge_sarif.py
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
"""Merge SARIF files into a single consolidated output.
Usage:
uv run --no-project merge_sarif.py RAW_DIR OUTPUT_FILE [--important] [--scans scans.json]
Reads *.sarif files from RAW_DIR (e.g., $OUTPUT_DIR/raw), produces
OUTPUT_FILE (e.g., $OUTPUT_DIR/results/results.sarif) containing all
findings merged and deduplicated.
--scans names the scans.json run-scans.sh wrote, and drops the SARIF files
belonging to scans recorded under .failed. Those files exist because a scan
that died part-way may still have written one, so without this a single dead
scan takes the whole run's deliverable with it: under --important its output
has no post-filter beside it, and that is an error rather than an empty
filter. Pass it whenever scans.json exists.
--important restricts the merged output to the findings that survived the
important-only post-filter. That filter reads semgrep's JSON metadata
(category/confidence/impact), which SARIF does not carry, so it cannot be
re-run against SARIF. Finding identity can be matched across the two formats
though, and that is what this flag does. Without it the merged SARIF in
important-only mode keeps every finding the mode exists to exclude.
The merge is pure Python and shells out to nothing. It used to try
`npx @microsoft/sarif-multitool` first, which made the output depend on
whether that package happened to be in the npx cache: the two backends do not
agree. Only this one dedups results on (ruleId, uri, startLine), which is the
identity --important matches against and the reason the report is told to
count from the merged file rather than sum per-scan totals. Multitool also
normalizes artifactLocation.uri, which would leave --important matching
nothing and blaming a semgrep format change that never happened.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
Key = tuple[str, str, int]
def sarif_key(result: dict) -> Key:
"""Identity of one SARIF result: rule, file, line.
The same triple in semgrep's JSON is (check_id, path, start.line), verified
field-for-field against semgrep output. Both the merge dedup and the
--important filter read it from here so the two cannot drift apart. The
filter is only correct while its keys are the keys the merge produced.
"""
locations = result.get("locations", [])
uri = ""
start_line = 0
if locations:
phys = locations[0].get("physicalLocation", {})
uri = phys.get("artifactLocation", {}).get("uri", "")
start_line = phys.get("region", {}).get("startLine", 0)
return (result.get("ruleId", ""), uri, start_line)
def json_key(result: dict) -> Key:
"""The same identity read out of a semgrep JSON result."""
return (
result.get("check_id", ""),
result.get("path", ""),
result.get("start", {}).get("line", 0),
)
def failed_sarifs(scans_json: Path) -> set[Path]:
"""Resolved paths of the SARIF files belonging to scans that did not succeed.
run-scans.sh records a failed scan carrying the same paths a success does, because a scan
that crashed part-way may still have written a file. What it wrote is not a scan result:
it is whatever semgrep produced before it stopped. Those files must not be held to the
post-filter requirement, or one dead process denies every healthy scan a deliverable.
"""
data = json.loads(scans_json.read_text())
failed = data.get("failed")
if not isinstance(failed, list):
raise ValueError(
f"{scans_json} has no .failed array; it is not a scans.json written by run-scans.sh"
)
return {
Path(entry["sarif"]).resolve()
for entry in failed
if isinstance(entry, dict) and entry.get("sarif")
}
def surviving_keys(sarif_files: list[Path]) -> set[Key]:
"""Keys kept by the important-only post-filter, one *-important.json per SARIF.
Derived from the SARIF files going into the merge rather than by globbing
*-important.json, so a post-filter that ran on only some of them is an error
here instead of a merged SARIF quietly missing whole rulesets.
Raises ValueError when a filter file is missing or unreadable. Filtering
against a partial key set drops real findings from the primary deliverable
and there is nothing downstream that could notice. That is stricter than
the merge itself, which warns and skips a SARIF file it cannot parse: an
unparseable SARIF contributes no findings either way, while an unparseable
filter file silently removes findings the SARIF does contain.
"""
keys: set[Key] = set()
missing: list[str] = []
for sarif_file in sarif_files:
filtered = sarif_file.with_name(f"{sarif_file.stem}-important.json")
if not filtered.is_file():
missing.append(filtered.name)
continue
try:
data = json.loads(filtered.read_text())
except json.JSONDecodeError as e:
raise ValueError(f"{filtered} is not valid JSON: {e}") from e
results = data.get("results")
if not isinstance(results, list):
raise ValueError(
f"{filtered} has no .results array; it is not a filtered semgrep result file"
)
for result in results:
keys.add(json_key(result))
if missing:
raise ValueError(
f"{len(missing)} of {len(sarif_files)} scans have no post-filtered JSON "
f"({', '.join(sorted(missing)[:5])}"
f"{', ...' if len(missing) > 5 else ''}). Run the important-only post-filter "
"over every file in the raw directory first."
)
return keys
def filter_to_keys(merged: dict, keys: set[Key]) -> tuple[int, int]:
"""Keep only results whose identity survived the post-filter. Returns (kept, dropped)."""
kept = 0
dropped = 0
for run in merged.get("runs", []):
keeping = []
for result in run.get("results", []):
if sarif_key(result) in keys:
keeping.append(result)
kept += 1
else:
dropped += 1
run["results"] = keeping
return kept, dropped
def merge_sarif_pure_python(sarif_files: list[Path]) -> tuple[dict, list[str]]:
"""Merge every SARIF into one run, deduplicating results by sarif_key.
Returns (merged, unparseable). The unparseable list is returned rather than just
warned about: a scan can exit 0, write a valid .json that puts it in .scans with a
finding count, and still leave a truncated .sarif. Dropping that file silently makes
results.sarif short by exactly those findings with nothing anywhere pointing at it.
The dedup is what makes the merged total meaningful: one finding flagged by two
rulesets is one row here and two in a sum of per-scan counts.
"""
merged = {
"version": "2.1.0",
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"runs": [],
}
seen_rules: dict[str, dict] = {}
all_results: list[dict] = []
seen_results: set[Key] = set()
tool_info: dict | None = None
skipped_files: list[str] = []
for sarif_file in sorted(sarif_files):
try:
data = json.loads(sarif_file.read_text())
except json.JSONDecodeError as e:
print(f"Warning: Failed to parse {sarif_file}: {e}", file=sys.stderr)
skipped_files.append(str(sarif_file))
continue
for run in data.get("runs", []):
if tool_info is None and run.get("tool"):
tool_info = run["tool"]
driver = run.get("tool", {}).get("driver", {})
for rule in driver.get("rules", []):
rule_id = rule.get("id", "")
if rule_id and rule_id not in seen_rules:
seen_rules[rule_id] = rule
for result in run.get("results", []):
dedup_key = sarif_key(result)
if dedup_key in seen_results:
continue
seen_results.add(dedup_key)
all_results.append(result)
if all_results:
merged_run = {
"tool": tool_info or {"driver": {"name": "semgrep", "rules": []}},
"results": all_results,
}
merged_run["tool"]["driver"]["rules"] = list(seen_rules.values())
merged["runs"].append(merged_run)
return merged, skipped_files
def main() -> int:
argv = sys.argv[1:]
important = "--important" in argv
argv = [a for a in argv if a != "--important"]
scans_json: Path | None = None
if "--scans" in argv:
i = argv.index("--scans")
if i + 1 >= len(argv):
print("--scans needs the path to scans.json", file=sys.stderr)
return 1
scans_json = Path(argv[i + 1])
del argv[i : i + 2]
if len(argv) != 2:
print(
f"Usage: {sys.argv[0]} RAW_DIR OUTPUT_FILE [--important] [--scans scans.json]",
file=sys.stderr,
)
return 1
raw_dir = Path(argv[0])
output_file = Path(argv[1])
if not raw_dir.is_dir():
print(f"Error: {raw_dir} is not a directory", file=sys.stderr)
return 1
# Collect SARIF files from raw directory only
sarif_files = sorted(raw_dir.glob("*.sarif"))
print(f"Found {len(sarif_files)} SARIF files to merge in {raw_dir}")
if not sarif_files:
print("No SARIF files found, nothing to merge", file=sys.stderr)
return 1
# Before the post-filter requirement below, so a dead scan is dropped rather than held to it.
if scans_json is not None:
try:
excluded = failed_sarifs(scans_json)
except (OSError, json.JSONDecodeError, ValueError) as e:
print(f"Error: {e}", file=sys.stderr)
return 1
kept = [p for p in sarif_files if p.resolve() not in excluded]
if len(kept) != len(sarif_files):
names = sorted(p.name for p in sarif_files if p.resolve() in excluded)
print(
f"excluding {len(sarif_files) - len(kept)} SARIF file(s) from failed scans: "
f"{', '.join(names)}"
)
sarif_files = kept
# Every scan failed. Writing an empty merge here would report a clean run over nothing.
if not sarif_files:
print(
f"Error: every scan in {scans_json} failed, so there is nothing to merge",
file=sys.stderr,
)
return 1
# Resolved before the merge, not after: a post-filter that did not run is a broken run,
# and finding that out first costs nothing while finding it out afterwards means either
# a wrong results.sarif on disk or a merge thrown away.
keys: set[Key] = set()
if important:
try:
keys = surviving_keys(sarif_files)
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
print(f"important-only: {len(keys)} findings survived the post-filter")
# Ensure output directory exists
output_file.parent.mkdir(parents=True, exist_ok=True)
merged, unparseable = merge_sarif_pure_python(sarif_files)
if unparseable:
# stdout, alongside the --scans exclusions, because that is the stream the Report phase
# reads and it must give these their own section. On stderr this was invisible to the
# summary: the scan sits in .scans as a success carrying its finding count, so nothing
# in scans.json or results.sarif shows that those findings went missing.
names = ", ".join(sorted(Path(p).name for p in unparseable))
print(
f"unparseable: {len(unparseable)} of {len(sarif_files)} SARIF files could not be "
f"parsed and are missing from the merge: {names}"
)
# Nothing was read at all, so "0 findings" would be a clean run rather than a broken one.
if unparseable and len(unparseable) == len(sarif_files):
print(
f"Error: none of the {len(sarif_files)} SARIF files could be parsed; "
"there is nothing to merge",
file=sys.stderr,
)
return 1
if important:
before = sum(len(run.get("results", [])) for run in merged.get("runs", []))
kept, dropped = filter_to_keys(merged, keys)
print(f"important-only: kept {kept} of {before} merged findings, dropped {dropped}")
# Unreachable while the two formats agree: every key came from the *-important.json
# sibling of a SARIF in this merge, so at least one must match something. Reaching it
# means sarif_key and json_key are no longer reading one identity out of two shapes —
# semgrep changed an output format — and every finding was dropped for that reason
# rather than by the filter. Writing anyway ships an empty results.sarif that reads as
# a clean important-only run, which is the failure this whole flag exists to prevent.
# Guarded on keys: a filter that legitimately kept nothing is a real zero, not drift.
if keys and before and not kept:
print(
f"Error: the post-filter kept {len(keys)} findings and the merge read {before}, "
"but none of them matched. The JSON and SARIF records of a finding no longer "
"reduce to the same (rule, file, line), so refusing to write a zero-finding "
f"{output_file}. Compare a raw *.json against its *.sarif in {raw_dir}.",
file=sys.stderr,
)
return 1
result_count = sum(len(run.get("results", [])) for run in merged.get("runs", []))
print(f"Merged SARIF contains {result_count} findings")
# Write output
output_file.write_text(json.dumps(merged, indent=2))
print(f"Written to {output_file}")
return 0
if __name__ == "__main__":
sys.exit(main())
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 or Trail of Bits.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download semgrep.zip