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://mp-staging.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.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download semgrep.zip