
Writing a Semgrep Rule for Your Own Codebase
The generic ruleset finds generic bugs. The rule you write for your own mistake finds it forever.
The generic ruleset finds generic bugs, and after the first run you have fixed most of what it will ever tell you. The rule you write for your own mistake finds that mistake forever, in every project, including the one you have not started yet.
That is the part of static analysis that compounds.
Why the syntax matters
Semgrep rules look like the code they match, which means you can write one without learning an AST query language:
rules:
- id: client-supplied-user-id
pattern: |
$DB.$TABLE.findMany({ where: { userId: $REQ.query.$X } })
message: >
Filtering by a client-supplied user id. Use the session identity
(req.user.id) — this endpoint returns other users' records.
languages: [javascript, typescript]
severity: ERROR
Ten minutes, and the most common serious bug I find in small-team codebases can never reach main again.
The rules worth writing first
Look at your own bug history. The three or four mistakes you have actually made are worth more than a hundred rules for mistakes you have not.
Mine, roughly:
- Any query filtered by an identifier taken from the request instead of the session
v-htmlordangerouslySetInnerHTMLwith a non-constant argument- A
fetchto a third-party API from client-side code with a key in scope Math.random()anywhere near a token, code, or identifier- A new route file with no schema validation on the body
Each one is a rule of a few lines and each one has caught something real.
Running it usefully
In CI on the diff, not the whole repository. Scanning everything on every pull request produces a wall of pre-existing findings that people learn to ignore, which is how static analysis dies in a team.
Diff-scanning means the only findings you see are the ones you just introduced, and that is a signal people act on.
Locally, a pre-commit hook on changed files catches things before they are even pushed.
Managing false positives
They will happen. Handle them explicitly with an inline suppression that includes a reason:
// nosemgrep: client-supplied-user-id -- admin route, authorised above
A suppression with a justification is documentation. A blanket disabled rule is a rule you have deleted.
Resources
- Repo: semgrep/semgrep
- Rule playground: semgrep.dev/playground
- Video walkthroughs: YouTube: Semgrep custom rules
- Related: Security by design when you are the whole team
Need this built properly?
I build secure, fast, bilingual platforms for clients across Egypt, Saudi Arabia, the UAE and Kuwait.


