Fast, open-source static analysis (SAST) that finds bugs and security issues by matching code patterns — not fragile regex. You'll run your first scan, read findings, write custom rules that understand code structure, wire Semgrep into CI on the diff only, autofix issues, and baseline an existing repo so the team actually adopts it. Hands-on throughout.
The journey:
| You need | Why |
|---|---|
The semgrep CLI |
Run scans and test rules locally |
| A terminal | Everything starts on the CLI |
| A code repo to scan | Any language Semgrep supports (30+) |
Install with pipx install semgrep (or brew install semgrep, or run the official Docker image). Semgrep needs no build, no compilation database, and no cloud account for local use. Confirm: semgrep --version.
Semgrep scans source code for patterns you describe in a small YAML DSL. The key difference from grep is that it matches the abstract syntax tree (AST), not text — so a single pattern matches regardless of formatting, variable names, whitespace, or comments.
grep "eval(" -> misses eval (x), and matches "eval(" in a comment
semgrep 'eval(...)' -> matches the real call, ignores comments and spacing
It runs per-file and per-function without needing to build your project, which makes it fast enough for a pre-commit hook and a pull-request gate. It ships thousands of community rules and lets you write org-specific ones in minutes.
The fastest start uses the auto config, which picks rules based on the languages it detects:
# Scan the current directory with recommended rules
semgrep scan --config auto
# Scan with a specific curated ruleset
semgrep scan --config p/ci
# Scan one language's security rules
semgrep scan --config p/python
--config accepts a registry shortcut (p/...), a local file, a directory of rules, or a URL. p/ci is a good, low-noise default for pipelines. The scan prints findings grouped by rule, with the file, line, and a message.
A finding tells you the rule, severity, location, and usually a fix suggestion. Output in machine-readable form for tooling:
semgrep scan --config p/ci --sarif --output semgrep.sarif # for code scanning UIs
semgrep scan --config p/ci --json --output semgrep.json # for scripts
semgrep scan --config p/ci --gitlab-sast # GitLab format
Curated rulesets worth knowing: p/ci (broad, low false positives), p/security-audit, p/secrets, p/owasp-top-ten, and language packs like p/javascript, p/golang, p/java. Combine your own rules with a curated pack rather than running everything unfiltered — the full community set is noisy by design.
The highest-value use of Semgrep is encoding your codebase's rules: banned internal APIs, unsafe helpers, missing auth checks. A rule is YAML with a pattern:
# rules/no-insecure-random.yml
rules:
- id: no-insecure-random-for-tokens
languages: [python]
severity: ERROR
message: >
Do not use random.random() for security tokens — it is not
cryptographically secure. Use the secrets module instead.
patterns:
- pattern: random.random(...)
Run it against test code:
semgrep scan --config rules/no-insecure-random.yml path/to/src
... is the ellipsis operator — it matches "any arguments" (or any statements). patterns: is a logical AND of conditions; pattern-either: is OR. Keep the message actionable: say what's wrong and what to do instead.
Metavariables ($X) capture and correlate parts of the match:
rules:
- id: hardcoded-password-compare
languages: [python]
severity: WARNING
message: Comparing against a hardcoded password literal.
patterns:
- pattern: $PW == "..."
- metavariable-regex:
metavariable: $PW
regex: (?i).*(pass|pwd|secret).*
Taint mode tracks data from an untrusted source to a dangerous sink — the real way to find injection:
rules:
- id: sql-injection
languages: [python]
severity: ERROR
message: Untrusted input flows into a SQL query.
mode: taint
pattern-sources:
- pattern: flask.request.args.get(...)
pattern-sinks:
- pattern: cursor.execute(...)
Autofix lets a rule rewrite code. Add fix: and run semgrep scan --autofix:
fix: secrets.token_hex(16)
No scanner is perfect. Suppress a single line inline with a reason:
token = random.random() # nosemgrep: no-insecure-random-for-tokens — test fixture only
For broader control, use a .semgrepignore file (same syntax as .gitignore) to exclude vendored code, generated files, and tests:
# .semgrepignore
vendor/
node_modules/
*_generated.go
Prefer a targeted nosemgrep: <rule-id> with a justification over a bare nosemgrep, which disables every rule on that line and quietly hides future issues.
The trick to a fast, quiet gate is scanning only what changed on a pull request, not the whole tree every time:
# .github/workflows/semgrep.yml
name: semgrep
on:
pull_request: {}
jobs:
semgrep:
runs-on: ubuntu-latest
container: semgrep/semgrep
steps:
- uses: actions/checkout@v4
- run: semgrep ci
semgrep ci is diff-aware: it compares against the base branch and reports only newly introduced findings, and it exits non-zero when a blocking rule matches — which is what fails the build. Keep the exhaustive full-tree scan for a nightly schedule.
Turning Semgrep on for a mature repo surfaces a wall of pre-existing findings. Blocking on all of them at once gets the gate disabled by lunchtime. Baseline instead:
# Only report findings introduced since a known-good commit
semgrep scan --config p/ci --baseline-commit <good-sha>
semgrep ci does this automatically against the merge base. The discipline: freeze the existing backlog, block only new code, then burn the backlog down deliberately in follow-up work. Adoption survives because developers are only ever asked to fix what they just wrote.
Test your rules like code with the built-in test runner: annotate example code with # ruleid: and # ok: comments and run semgrep --test.
p/ci plus your own rules.semgrep ci diff mode or scans get slow and get skipped.nosemgrep. Always suppress a specific rule id with a reason; a blanket ignore hides future bugs.semgrep scan --config auto on a repo and read three findings — rule, location, message.random.random() for tokens) and run it against a test file.$X metavariable and a metavariable-regex constraint.fix: to your rule and run --autofix on a copy of the file.semgrep ci job to a pull request and watch it report only the new finding.Self-check:
... ellipsis match, and how do patterns: and pattern-either: differ?You now have the loop: pattern → rule → metavariable/taint → autofix → CI on the diff → baseline. That's Semgrep as a fast, developer-owned SAST gate.