← All posts

Testing

Semgrep: Static Analysis for Every AI-Generated Code Block

Semgrep catches what AI code generators miss — security bugs, bad patterns, and policy violations before they ship.

A scanning beam highlights flagged code blocks in a static analysis review, representing Semgrep catching security issues in AI-generated code

Here is the workflow I used to have: AI assistant generates a function, I read it, it looks reasonable, I ship it. Here is the problem with that workflow: I am a human reading code at human speed, with human blind spots, after probably too much coffee. I will miss a SQL injection vector hiding inside an f-string. I will not notice that the JWT verification skips the signature check under one specific condition. I will not catch that a newly generated helper re-implements a pattern we already banned in a previous incident. Semgrep static analysis is the automated layer that catches exactly these things — consistently, at machine speed, on every single diff — and it has become non-negotiable in my review process for AI-assisted code.

What Semgrep Is and Why It Is Different

Semgrep is an open-source static analysis tool that lets you write rules in a syntax that closely mirrors the code you are actually trying to match. Unlike traditional regex-based linters, Semgrep is syntax-aware — it parses your code into an AST and matches patterns structurally, not textually. That means a rule targeting a dangerous eval() call will match it whether the argument is a string literal, a variable, or a multi-line expression. Whitespace, formatting, and variable naming do not fool it.

The project is maintained by Semgrep Inc. (formerly r2c), and the OSS CLI is genuinely free and open to self-host. There is a Pro tier with a proprietary engine and managed platform, but for the use case I care about — running rules locally and in CI on every PR — the open-source core is entirely sufficient.

The Core Problem: AI Code Is Statistically Average

Language models are trained to produce code that looks like the code they were trained on. The internet contains a lot of insecure code. It contains a lot of deprecated patterns, abandoned idioms, and copy-pasted anti-patterns that happened to work well enough to be committed and pushed. AI assistants reproduce these patterns fluently and confidently. They are not reasoning about security — they are predicting tokens.

This is not an argument against using AI coding tools. It is an argument for treating their output the way you would treat any code from an unfamiliar source: with a consistent, automated review pass before it earns trust. Semgrep is that pass.

Setting It Up: Local and CI in One Go

Installation is straightforward. On macOS:

brew install semgrep 

Or via pip, which works anywhere Python is available:

pip install semgrep 

The fastest way to get value immediately is to run Semgrep against your repo using the curated auto ruleset, which selects rules based on the languages it detects in your project:

semgrep --config=auto . 

That single command will scan your entire working tree against thousands of community-maintained rules covering security, correctness, and best practices across JavaScript, TypeScript, Python, Go, Java, Ruby, and more. On a mid-size Node.js repo, my first run took about 40 seconds and surfaced four real findings I had not noticed — two of them in AI-generated code from the previous week.

Writing Your Own Rules: The Part That Makes It Stick

The real power of Semgrep is not the built-in rules — it is that writing your own rules is genuinely approachable. The pattern language is just code with metavariables sprinkled in. Here is a rule I wrote after an AI assistant kept generating Express routes that passed req.query directly into a MongoDB query object — a classic NoSQL injection pattern:

# .semgrep/no-direct-query-passthrough.yaml rules: - id: no-req-query-in-mongo-find patterns: - pattern: $COLLECTION.find($REQ.query) - pattern: $COLLECTION.find({..., $KEY: $REQ.query.$VAL, ...}) message: > Passing req.query directly into a MongoDB query is a NoSQL injection risk. Validate and sanitize input before use. languages: [javascript, typescript] severity: ERROR metadata: category: security 

Run it with:

semgrep --config=.semgrep/ . 

Now every future AI-generated route gets checked against this rule automatically. This is the compounding value: the first time an AI generates a bad pattern, you write a rule. Every subsequent AI-generated file gets checked against that institutional memory.

Wiring It Into CI: GitHub Actions in Under Ten Lines

The point where Semgrep stops being a local habit and becomes a team enforcement layer is when it runs on every pull request. Here is a minimal GitHub Actions step:

- name: Semgrep scan uses: returntocorp/semgrep-action@v1 with: config: >- p/default p/owasp-top-ten .semgrep/ 

I layer three configs: the default ruleset, the OWASP Top Ten ruleset (which is particularly relevant for AI-generated API and auth code), and my local custom rules directory. The action fails the check on any ERROR-severity finding, which blocks the merge. WARNING-severity findings are surfaced as annotations but do not block — a deliberate choice so the pipeline does not become noise.

Specific Patterns I Catch on AI-Generated Code

After several months of running this in practice, these are the finding categories that appear most frequently in AI-generated code on my projects:

  • Hardcoded credentials and secrets. AI assistants love to fill in placeholder values that look suspiciously real. Semgrep's p/secrets ruleset catches these reliably.
  • Unsafe deserialization. Generated Python code frequently uses pickle.loads() on untrusted input without a second thought.
  • Missing authentication checks on generated routes. A new Express or FastAPI route appears, and the middleware chain is simply absent. Custom rules on your specific auth wrapper functions catch this pattern.
  • Deprecated crypto. MD5, SHA-1, DES — models trained on older codebases reproduce older crypto idioms. Semgrep's crypto rulesets flag these immediately.
  • Prototype pollution vectors in JavaScript. Deep object merge utilities generated by AI often reproduce the classic prototype pollution pattern.

Rough Edges and Real Limitations

Semgrep is excellent but it is not magic. Here is where it falls short in practice:

  • False positives on complex patterns. The more permissive your metavariable matching, the more noise you generate. Tuning rules to be specific enough to be useful without generating constant false positives takes iteration.
  • No cross-file taint tracking in the OSS tier. If a tainted value is passed through three functions across two files before it reaches a sink, the free version will not catch that full chain. The Pro engine handles this, but it is not open-source.
  • Python-only CLI means an extra dependency. On teams that do not already use Python tooling, getting Semgrep into the environment can meet resistance. The Docker image mitigates this.
  • Rule quality in community registries is uneven. The Semgrep Registry has thousands of rules, but not all of them are maintained or well-tested. Audit rules before adding them to a blocking CI step.
  • It is a linter, not a runtime guard. Semgrep catches patterns in source. It cannot observe behavior, race conditions, or logic errors that only manifest under specific runtime conditions. It is one layer, not the whole stack — pair it with something like OpenTelemetry for the runtime side.

On that last point — if you want the runtime complement to Semgrep's static layer, my post on OpenTelemetry for AI-built features covers exactly that pairing. Static analysis at commit time, distributed tracing in production: together they close most of the trust gap.

The Compounding Payoff

The first Semgrep run on a codebase is useful. The tenth run, after you have written rules specific to your stack, your auth patterns, and the mistakes your AI assistant tends to repeat — that is when it becomes genuinely valuable. The rule library becomes a codified record of every near-miss and every pattern your team has agreed to ban. AI assistants will keep generating code; Semgrep keeps ensuring that code has to clear a bar that gets higher over time, not lower.