Semgrep SAST skill for Codex
A ruleset you trust, gated on new bugs. This skill runs Semgrep to find injection, SSRF, secrets and unsafe deserialization in source, then does the work that makes SAST stick: triaging the first noisy run, suppressing false positives precisely, writing tested custom rules for project-specific footguns, and gating CI on new findings via a baseline so a backlog never disables the check.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- semgrep-sast Installs to ~/.agents/skills/semgrep-sast/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install semgrep-sast The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project semgrep-sast Installs to .agents/skills/semgrep-sast/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/semgrep-sast.zipunzip -o semgrep-sast.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) semgrep-sast PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $semgrep-sast, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Runs Semgrep static analysis to find security bugs in source — injection, SSRF, hardcoded secrets, unsafe deserialization, weak crypto — tunes the ruleset to cut false positives, writes custom rules for project-specific patterns, and gates CI on new findings only. Use when asked to set up SAST, add Semgrep to CI, write a custom rule, or triage a noisy Semgrep report. Not for dependency CVEs (SCA).
Use it when
- Setting up static analysis for security bugs in source
- Adding Semgrep to CI without blocking on an existing backlog
- Writing or tuning custom rules for project-specific patterns
Use something else when
- The target is vulnerable dependencies, not your source: use dependency-scanning
- You want deep dataflow queries on a compiled codebase: use codeql-analysis
- You are only hunting committed secrets: use secret-scanning
How to check the result
Re-run after fixes to confirm findings clear with no new ones, and prove each custom rule with a --test fixture that fires on the bad pattern and stays silent on the good one.
The mistake it prevents
Shipping the untriaged first run as a report, or gating CI on the whole backlog so the check gets disabled within a day.
Semgrep is valuable when its output is trusted — a small ruleset that fires on real bugs, custom rules for your own footguns, and a CI gate that blocks only new findings. A report no one reads because it is 90% noise is worse than no scan. Tune relentlessly.
Inspect first
Read AGENTS.md and detect languages. Semgrep ships curated rulesets; start narrow, not
with everything. Semgrep downloads rules from the registry by default (needs network) or
runs fully offline with --config pointing at a local rules dir — note which you use, since
Codex’s sandbox may block the registry.
Run
# Curated security rules for auto-detected languages (registry)
semgrep --config=p/security-audit --sarif -o semgrep.sarif .
# Owasp-focused + secrets
semgrep --config=p/owasp-top-ten --config=p/secrets .
# Offline: only your own rules
semgrep --config=./.semgrep/ .
Useful rule packs: p/security-audit, p/owasp-top-ten, p/secrets, p/ci, and
language packs (p/javascript, p/python, p/golang). p/default is broad and noisy —
prefer targeted packs.
Triage and tune
The first run is always noisy. Work the list down:
- Confirm or dismiss each finding by reading the code — is untrusted input reaching the sink, or is the match a false positive (sanitized, constant, test code)?
- Suppress precisely, never globally:
- Inline:
// nosemgrep: <rule-id>on the line, with a comment saying why. - Path: add a
paths:exclude in the rule or--exclude='tests/**'for whole trees. - Config: a
.semgrepignorefor generated/vendored code.
- Inline:
- Drop rules that do not fit your stack rather than suppressing every instance. Keep the ruleset small enough that a clean run means something.
Record a one-line reason for every suppression so the next person does not re-litigate it.
Custom rules — Semgrep’s real power
Generic rules miss project-specific footguns (a required auth decorator, a banned function, a tenant filter). Write a rule once and it enforces the pattern forever. Minimal rule:
# .semgrep/no-raw-sql.yml
rules:
- id: no-raw-string-sql
languages: [python]
severity: ERROR
message: Build queries with parameters, not string interpolation (SQLi risk).
patterns:
- pattern: $CURSOR.execute(f"...")
- pattern-not: $CURSOR.execute("...", ...)
Semgrep patterns match structure, not text: ... is a wildcard, $X a metavariable.
Develop rules fast with semgrep --config .semgrep/no-raw-sql.yml --test against a
*.py/*.yml fixture pair. For the pattern operators (pattern-either, pattern-inside,
metavariable-pattern, taint mode with pattern-sources/pattern-sinks) and worked
examples, read references/custom-rules.md when a generic rule
is too broad or too narrow.
CI gate — block new findings, not the backlog
A SAST gate that fails on 300 pre-existing findings gets disabled on day one. Gate on the diff:
# Fail only on findings introduced by this PR
semgrep ci --baseline-commit "$(git merge-base origin/main HEAD)"
semgrep ci understands baselines and SARIF upload. Set severity so only ERROR-level new
findings block; WARNING can report without failing. Hand off workflow mechanics to
github-actions-workflows and secret handling to secrets-in-ci.
Verification
- Re-run after fixes: the finding is gone and no new ones appeared.
- For each custom rule, prove it with a
--testfixture: it fires on the bad pattern and stays silent on the good one. A rule with no test will rot. - The CI gate fails on a PR that introduces an
ERRORand passes on a clean diff.
Output contract
## Semgrep SAST — <repo> (<date>)
Rulesets: <packs + custom>. Mode: <registry | offline>.
### Findings (confirmed)
| Rule | Severity | Location | Issue | Fix |
|------|----------|----------|-------|-----|
| sqli-interpolation | ERROR | db/user.py:88 | f-string SQL | parameterize |
### Suppressed (with reason)
- <rule@path> — false positive: input is a constant enum
### Custom rules added: <ids>. CI gate: baseline on main, fail on new ERROR.
Hard rules
- Tune before you ship a report; an untriaged dump is noise, not analysis.
- Suppress precisely with a reason; never disable a rule class to silence one false positive.
- Every custom rule needs a passing
--testfixture before it gates CI. - Semgrep finds source bugs, not vulnerable dependencies — hand off to dependency-scanning.
What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Semgrep SAST"
short_description: "A ruleset you trust, gated on new bugs"
default_prompt: "$semgrep-sast Set up Semgrep security scanning for this TypeScript repo" references/custom-rules.md
# Semgrep custom rules — operators and worked examples
Read this when a generic ruleset is too broad or too narrow and you need to express a
project-specific pattern. Semgrep matches code structure, not raw text, so these rules
survive reformatting and variable renames.
## Pattern primitives
| Syntax | Meaning |
|---|---|
| `...` | Ellipsis — matches any sequence (args, statements, members) |
| `$X`, `$FUNC` | Metavariable — binds to one node; the same name must match the same code |
| `"..."` | Any string literal |
| `$X.$METHOD(...)` | Any method call on `$X` |
## Rule operators
| Key | Use |
|---|---|
| `pattern` | Must match |
| `pattern-not` | Exclude matches of this (carve out the safe form) |
| `pattern-either` | OR across several patterns |
| `pattern-inside` | Only match when nested inside this context (e.g. inside a handler) |
| `pattern-not-inside` | Exclude matches inside this context (e.g. inside a test) |
| `metavariable-pattern` | Constrain a metavariable to match a sub-pattern |
| `metavariable-regex` | Constrain a metavariable by regex |
| `focus-metavariable` | Report only the span of this metavariable |
## Example 1 — require an auth decorator on routes
```yaml
rules:
- id: route-missing-auth
languages: [python]
severity: ERROR
message: Route handler is missing the @require_auth decorator.
patterns:
- pattern: |
@app.$METHOD("...")
def $HANDLER(...):
...
- pattern-not: |
@app.$METHOD("...")
@require_auth
def $HANDLER(...):
...
```
## Example 2 — ban a dangerous function
```yaml
rules:
- id: no-child-process-exec
languages: [javascript, typescript]
severity: ERROR
message: Use execFile with an argument array, not exec with a string (command injection).
pattern-either:
- pattern: require('child_process').exec(...)
- pattern: cp.exec(...)
```
## Example 3 — taint mode (source → sink)
Taint mode tracks data flow from an untrusted source to a dangerous sink and reports only
when they connect, which cuts false positives sharply.
```yaml
rules:
- id: taint-sql-injection
languages: [python]
severity: ERROR
message: Untrusted request data flows into a raw SQL execute().
mode: taint
pattern-sources:
- pattern: request.$ANY
pattern-sinks:
- pattern: $CURSOR.execute($QUERY)
pattern-sanitizers:
- pattern: sanitize(...)
```
## Testing a rule
Put a fixture next to the rule: `no-raw-sql.yml` + `no-raw-sql.py`. Mark expected hits with
a `# ruleid: <id>` comment on the offending line and `# ok: <id>` on a safe line, then:
```bash
semgrep --test --config .semgrep/
```
The test passes only when the rule fires exactly on the `ruleid` lines and stays silent on
the `ok` lines. A rule without a passing test should not gate CI — it will drift into noise
or silence.
## Keeping the set healthy
- One rule, one footgun. Narrow rules are easier to trust and tune.
- Prefer `pattern-not` to carve out the safe form over suppressing every instance later.
- Review custom rules when the framework or its API changes; a rule matching an old API
silently stops protecting you.
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.