Install one plugin, catch a real bug
npm i -D eslint eslint-plugin-postgresql-security
// eslint.config.mjs
import pg from 'eslint-plugin-postgresql-security';
export default [pg.configs.recommended];
// app.js
import { Pool } from 'pg';
const pool = new Pool();
export async function getUser(id) {
return pool.query(`SELECT * FROM users WHERE id = '${id}'`); // ← interpolated
}
app.js
5:21 error 🔒 Unsafe SQL query construction detected (template literal). | CRITICAL
Fix: Use parameterized queries ($1, $2) instead of interpolating values.
https://owasp.org/www-community/attacks/SQL_Injection postgresql-security/no-unsafe-query
That message is the whole thesis. Severity so you can triage it, the fix so a human doesn't have to go looking, and a citation so an LLM asked to "fix the lint errors" produces a parameterized query instead of a plausible-looking escape helper. Machine-readable equivalents ship via the SARIF formatter for GitHub code scanning.
The ecosystem
30 published plugins · 477 rules — 21 security plugins (272 rules), 7 code-quality plugins (107 rules), 2 React plugins (98 rules). Counts generated 2026-08-13 from the source tree — never hand-typed.
Every package is versioned, released, and installable on its own. Take the two that match your stack; ignore the other twenty-eight.
🔒 Security — languages, platforms & SDKs
18 plugins · 220 rules
🏗️ Security — application frameworks
3 plugins · 52 rules
🧭 Architecture
1 plugin · 55 rules
✨ Code quality
6 plugins · 52 rules
⚛️ React
2 plugins · 98 rules
Supporting tools
The table and counts above are generated from the source tree by
npm run sync:root-readme, and CI fails if this file drifts from it. The canonical counts contract isinterlace-numbers.json.
Why an independent ecosystem?
Every rule here is a clean-room implementation — familiar naming, different engineering.
The deeper case — why any of this is worth money:
cicd-impact/value-philosophy.md— what value is, the two ruling systems (capitalism + humanism), and the unbroken chain from human incentives down to a single ESLint rule.cicd-impact/philosophy.md— how CI/CD friction expresses itself along money, velocity, and deliverability.cicd-impact/— a forkable calculator that turns that into a$/CI-minutefigure for any GitHub Actions repo.
How we measure quality (FP / FN / TP / TN)
Every finding is one of four things, and we track all four — per rule, per CWE, per OSS repo:
- Precision = TP / (TP + FP) — of what we flag, how much is real?
- Recall = TP / (TP + FN) — of what we should flag, how much did we catch?
- F1 — the harmonic mean, so neither can be gamed alone.
Signal over noise. A rule that fires two thousand times to be right four hundred of them does not make a codebase safer — it teaches the team to skip the category, and an ignored tool has zero recall regardless of what it detects.
This is a constraint we hold against ourselves. no-unsafe-regex-construction has a real
~300-file recall gap: competitors catch new RegExp(userSuppliedName) and we do not. We
built the fix and measured it before shipping — findings went 29 → 2,243 on the same
corpus, and a hand-read of the new ones put precision at ~25%. We reverted it, and
documented the gap rather than closing it badly.
The words we use, in plain terms
Every number below depends on these, so they are defined once here rather than assumed.
Severity — what ESLint does when a rule fires. You set it per rule in your config, and our presets set it for you:
That is why a severity is a promise: error interrupts you and warn does not.
The four outcomes. Every finding is one of these, and the pair of words for the two that matter:
- TP (true positive) — the rule fired and the code really was vulnerable. Signal.
- FP (false positive) — the rule fired and the code was fine. Noise.
- FN (false negative) — the code was vulnerable and the rule stayed quiet. A miss.
- TN — the code was fine and the rule stayed quiet. Correct silence.
Precision = TP / (TP + FP). Of the findings you are shown, what share are real? This is the number that decides whether you keep the tool on. Recall = TP / (TP + FN) — of the real problems, what share did we catch? You cannot maximise both, which is the tradeoff section below.
n — the sample size. How many individual findings we pulled out of real
repositories and read one at a time, labelling each TP, FP, or undecidable. n = 20 means twenty findings judged by hand. It matters because a percentage from
a small n is mostly luck: at n = 4, reclassifying one finding moves precision
by 25 points.
Sampled precision vs fixture precision. Fixture precision is measured on test files we wrote — it is a regression gate and says nothing about your code. Sampled precision is measured on open-source repositories we did not write. Only the second one predicts your afternoon, and it is the only one the bars below use.
The severity contract — what each tier promises
Nobody in this space publishes a precision bar, so we set ours and publish where we sit against it. A severity is a promise about your triage time, and it is the only part of a lint finding a consumer plans around:
Severity signals urgency, not confidence. warn used to carry a lower
accuracy bar here — 70%, i.e. three wrong findings in ten. That was incoherent:
a noisy warn costs a reader exactly as much attention as a noisy error, and
the only difference is whether the build also fails. Anything we switch on for
you meets the same bar; the tier tells you how soon to care.
And the bar that matters is the preset's, not the rule's. You install a config, not a rule. Twenty rules each sitting at a defensible 5% do not compose into a 5% experience, so the aggregate carries its own budget.
Why 5%, and why "effective"
The number is not ours. Google admits an analyzer into code review only while its effective false-positive rate stays under 10%, and its actual fleet average runs just under 5% — measured across the largest continuously-linted codebase anyone has published on. Above roughly 10%, developers stop reading a tool, then switch it off. We take the fleet average rather than the ceiling because we are asking you to enable a preset you did not tune.
Effective is the load-bearing word, also Google's. A finding counts against us when it is wrong — and when it is right but you would not act on it: the message did not explain itself, or the issue was too trivial to be worth the interruption. A correct finding nobody understands costs a reader exactly what a wrong one costs. That makes message quality a precision property, not polish.
Three rules follow from that, and they bind us more than they bind anyone else:
- A rule may not ship at a tier it has not been measured against. Unmeasured means opt-in, however good it looks on our fixtures.
- The sample is for that rule specifically, on code we did not write, each finding labelled TP / FP / undecidable with a reason (protocol). A rule's precision measured inside a mixed sample of four rules is not that rule's precision.
- A rule that misses its bar is a defect, and gets fixed. Demotion is the
stopgap that protects you today —
no-unsafe-deserializationwenterror→warnon 2026-05-09 at 76% Wild hits, locked by a test so it cannot quietly return — but a demoted rule is an open defect, not a resolved one. Shipping below the bar of the tier you sit at is not an accepted state here; see the open list and the scoreboard below.
Reachability is how the bar gets met
A 5% bar is not reached by tuning thresholds. It is reached by changing what a rule is willing to assert.
Reachability — can attacker-controlled input actually arrive at this expression — is the lever the commercial tools pulled to cut alert volume by a reported 92–95%, and it is the difference between the two contracts a rule can have:
The first is not a stricter version of the second — it is a different product.
detect-object-injection ships the first contract and produces 14,696
findings across our 20-repository corpus at 0 confirmed true positives in the
sample. No threshold fixes that; only the contract does.
So: anything enabled by default asserts reachability. A rule that cannot resolve a path from an untrusted source to the sink stays quiet by default, and the paranoid sweep — report on shape alone — remains available opt-in, with its rate published. This is what our taint analysis already approximates; the change is making it the precondition rather than the tiebreak.
When a rule is too rare to score — the census criterion
A ratio needs findings to compute. detect-non-literal-fs-filename produces
one finding across 3.04M lines of mature open-source code, and that finding
is real. There is no sample size at which that becomes 70% — at n = 1 the
defensible lower bound is 20.7%, and the bar needs n ≥ 9. A rule can be
correct and still be unscoreable.
So a rule whose whole output on the corpus is smaller than the n its tier
requires is scored a different way, and it is a stricter test, not a waiver:
Sampling lets a rule hide its worst findings behind the ones you happened to
draw. A census cannot: one false positive anywhere in the output fails it. That
is why this is not the easy road — detect-non-literal-fs-filename needed three
rounds of fixes to get from 37 findings to 1, and any of the 36 removed would
have failed it.
A rule may not choose which criterion it is judged by. The finding count decides, and the count is published beside the verdict.
How big a sample the bar actually needs
A percentage from 20 findings is not evidence for a 95% claim. If you read 20
findings and all 20 are real, the true rate could still be 85% and you got a good
draw. So the bars carry a required n, derived from the Wilson score interval —
the standard statistical answer to "given what we saw, what is the worst the true
rate plausibly is?", at 95% confidence. That worst case is the number we hold
ourselves to, not the raw percentage.
With a perfect sample — every finding a true positive:
So the error bar needs n ≥ 73 with zero false positives, or n ≥ 150 with one.
n ≥ 20 — §A2's minimum — is enough to publish a rate, never enough to clear the
error bar. We got this wrong when the contract was first written and are saying so
rather than quietly restating it.
What these bars cost — the tradeoffs
Every one of these choices buys something and spends something. In order of how much it will annoy you:
A 95% error bar means very few rules will ever be error. Most real security
coverage therefore lives at warn or behind an opt-in preset, and a team that wants
breadth has to go and enable it. We think a small trustworthy error set beats a
large one you learn to skip — but if your threat model wants maximum coverage at any
noise level, our defaults are the wrong shape for you and strict is one line away.
Raising precision costs recall, and we have paid it. A precision sweep in this
repo took false positives 10 → 3 while false negatives went 18 → 34. We shipped a
recall fix for no-unsafe-regex-construction that took findings 29 → 2,243 at ~25%
precision and reverted it, documenting the gap instead. A high bar systematically
biases us toward missing things. That is a deliberate trade, not an oversight, and the
misses are published alongside the wins.
A 5% bar cuts against recall, and we measure the cut. Buying precision by going quiet is the mirror failure, and we have committed it: one sweep took false positives from 10 to 3 while false negatives went from 18 to 34, and the precision number alone read as progress. So precision is never reported without Youden's J — sensitivity + specificity − 1 — the OWASP Benchmark's headline score, which a tool cannot raise by reporting more OR by reporting less. It is the guard against optimising the number we happen to be looking at.
Opt-in rules have no floor, and some are genuinely noisy. detect-object-injection
measured 0/13 on real code. It stays exported because a team hunting prototype
pollution specifically may want it — but "no floor" means what it says, and turning on
every rule will hand you our worst numbers.
Measurement is the bottleneck, not analysis. 73 hand-labelled findings per
error-tier rule, across 121 rules, is thousands of judgements. That is why 117 rules
are unscored, and why they get measured in descending order of how loud they are on
real code rather than in the order we find them interesting. Expect this scoreboard to
fill in slowly.
We label our own findings. The person deciding TP or FP wrote the rule, which is a real bias. The mitigation is that every label ships with its reason and its file and line, so you can disagree with any single one and recompute (the labelled samples). It is not independent review, and we do not call it that.
Where we actually sit, 2026-08-17
Honest status, because a published bar with no scoreboard is marketing:
That table is the current state of a programme, not a boast. Four rules have been through per-rule measurement on real code; 117 have not. The ReDoS row says unscored rather than a percentage because the generic timing sweep that produced it can confirm a slow pattern and cannot prove a fast one — two patterns it called flat were 167 ms and 956 ms under a crafted input. A method that only errs one way does not get to report a precision figure.
Every row above that sits below its tier's bar is an open defect with an owner.
detect-non-literal-fs-filename ships at warn against a 70% bar and has no
qualifying measurement; no-redos-vulnerable-regex ships at error unscored. Neither
is an accepted state. The work is: raise the sample to the size the tier needs, fix
what the sample exposes, re-measure, and demote in the meantime if the gap is wide.
Rules below their bar block new rule work — we would rather have ten rules that hold
their promise than a hundred that do not.
Why publish this at all. Because the alternative is the industry norm: ship 121
rules at error, publish an F1 from your own fixtures, and let the user discover the
noise. Our corpus F1 is 100% and our measured real-code precision is 28.6% — those
are both true, and only one of them predicts your afternoon.
The same corpus, six plugins, one command
Every community ESLint security plugin, scored on identical labelled fixtures from
benchmarks/corpus/ — our own CWE corpus, not NIST Juliet; the suite that scores it was
renamed for exactly that reason:
Read that with the caveat attached: the fixtures are ours, so a perfect score there is
a regression gate wearing a benchmark's clothes. The number that survives contact with code
we did not write is the away-turf one — 51/51 live cases on eslint-plugin-security's own
RuleTester suite, which they wrote to define their own true positives.
And on 20 open-source projects (21,146 files, 3.04M LOC), sampled and hand-labelled on both
sides, recommended vs recommended:
28.6% is not a good number and we are not going to dress it up. Roughly two in three of our findings on real code are wrong. It beats the incumbent by 2.2× and that is the only claim it supports.
It replaces a 67% we published on 2026-08-14. That figure was not falsified — the sample was. §A2 requires stratifying across the top 5 rules by volume, and the older sample did not, so it under-weighted exactly the rules that produce most of the output. Re-sampled correctly, the number fell. The measurement got better; the plugins did not get worse.
The noise is concentrated, not diffuse, which is the actionable part:
no-unlimited-resource-allocation scored 0 TP / 5 FP and is the loudest rule in
recommended; no-toctou-vulnerability scored 0 TP / 4 FP and never names the check
its reported use races. Two rules, most of the noise. Every finding is labelled with
its reason in
SAMPLED-FP-2026-08-17.md,
including the ones that embarrass us.
What this is not
- Not SAST. No inter-procedural dataflow, no cross-file taint, no build integration, no SBOM, no secret-history scanning. That is a different product at a different price.
- 75 CWEs, of roughly 900 — the ones an AST can see.
- "Quieter" is measured against
eslint-plugin-security. Against a narrow single-purpose plugin such aseslint-plugin-no-unsanitizedwe report more, and we say so.
Full results · Criteria · Methodology and exact rule lists · Raw data
That is what CLAIMS.md is for: every marketing claim in this repo
maps to the evidence file that produced it, carries a verification date, goes
stale after 90 days, and is withdrawn in public when it doesn't hold.
npm run audit:claims fails the build if a withdrawn claim reappears anywhere.
Performance — measured weekly, not claimed
Cold = --no-cache. Warm = --cache, primed — the number you feel on
every save and every CI run.
Interlace on oxlint runs the same rulesets through the oxlint engine via our
JS-plugin shims. Same rules, different engine.
Head-to-head, by job
Whole-plugin comparisons mislead in both directions: a plugin bundles jobs its rival does not have, so an aggregate delta is partly a difference in scope rather than speed. The unit of comparison here is a job — a concrete capability — with the specific rules named on both sides.
Two of those rows are losses. They stay: a table where every row favours us is
authored, not measured. The full breakdown — including where competitors are
genuinely better — lives in
benchmarks/suites/ilb-headline/matchups.ts,
where every cited rule is CI-verified to exist.
Who we compare against
Named, versioned, and linked — so you can check we did not pick a weak opponent:
eslint-plugin-security ·
eslint-plugin-sonarjs ·
@microsoft/eslint-plugin-sdl ·
eslint-plugin-no-unsanitized ·
eslint-plugin-security-node ·
eslint-plugin-no-secrets ·
eslint-plugin-regexp ·
eslint-plugin-import
How we benchmark the benchmark
- Scope: only our SDK-agnostic plugins —
secure-coding,node-security,browser-security,import-next. Framework-bound plugins (pg, jwt, nestjs-security…) have no comparable competitor; an uncontested win there tells you nothing. - Corpus: two real repos — nestjs (Node) and shadcn-ui (frontend), shallow-cloned at a recorded commit SHA.
- Same file set: every stack lints an identical glob, and parity is asserted — a run where the stacks saw different files is refused, not published.
- Median of N after a discarded warmup, with min–max spread recorded so a noisy machine is visible rather than hidden.
- Failures are recorded, never dropped. A crash cannot be timed as a fast run; a stack that processed 0 files fails the gate instead of rendering an impossibly fast bar.
- oxlint built-ins run a different rule scope (no secrets, injection, or CSP analysis). Shown for context and excluded from "fastest" highlighting — a different job, not a peer.
Every number is regenerated by
weekly-benchmark.yml (Mondays,
09:00 UTC) on public runners, stored append-only, and published as the badges
above. Reproduce locally: npm run ilb:headline -- --repo=nestjs --repeat=5.
Compatibility
ESLint
Last refresh: 2026-08-02 (source: npm registry —
npm run stats:eslint-versions)
Supported majors cover 90.49% of weekly ESLint downloads. Every published package
declares "eslint": "^8.40.0 || ^9.0.0 || ^10.0.0".
Why the floor is 8.40 and not 8.0: releases before 8.40 predate
context.sourceCode / context.filename, which this repo reads at 333 call sites.
Measured on [email protected], ESLint 8.0.0 and 8.39.0 throw on
load and 8.40.0 works — so the range now states the oldest minor the rules actually
run on (#407).
When a major gets supported: either it holds ≥20% of weekly npm downloads, or it is the next major after a currently-supported one (we ship support pre-emptively so you can upgrade ahead of the curve, not behind it). A major is dropped only after two consecutive refreshes below the gate and a supported successor exists. Full policy: docs/ESLINT_VERSION_SUPPORT.md.
Node.js
Who this is for
Contributing & security
- 💡 Have an idea? Start a discussion
- 🐛 Found a bug? Open an issue — a false positive is a bug here, and it's the bug we most want to hear about
- 🛠️ Want to contribute? CONTRIBUTING.md · CODE_OF_CONDUCT.md · GOVERNANCE.md · ARCHITECTURE.md
- 🔒 Found a vulnerability? Don't open a public issue — SECURITY.md has the private disclosure path
Also worth reading: ROADMAP.md for what's next, and AGENTS.md / CLAUDE.md if you're pointing an AI agent at this repo.
Design system
The UI in this repo's docs site is built on
Interlace — components and
tokens ship from @interlace/ui, and the look-and-feel doctrine (layout,
typography, colour, motion, a11y, keyboard, …) is authored there, not here.
Browse it at storybook.interlace.tools
→ Philosophy, or read the sources in
docs/philosophies/.
Links
License
MIT © Ofri Peretz — see LICENSE.