The Interlace ESLint Ecosystem — security and code-quality rules that explain themselves,
to your team and to the AI writing half your code.
Every finding ships with a CWE, a CVSS score, an OWASP mapping, and the fix.
⭐ Star the repo · 👀 Watch releases · 📨 Follow the writeups · 📊 Live metrics
If these plugins caught a real bug for you, a star is the signal that keeps the ecosystem maintained.
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 · 465 rules — 21 security plugins (260 rules), 7 code-quality plugins (107 rules), 2 React plugins (98 rules). Counts generated 2026-08-05 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 · 209 rules
| Package | Rules | What it catches | Docs | Downloads |
|---|---|---|---|---|
eslint-plugin-browser-security |
45 | Detects DOM XSS, postMessage abuse, tokens in localStorage, insecure cookies, clickjacking, mixed content, and CSP gaps | docs | |
eslint-plugin-node-security |
37 | Detects command injection, path traversal, SSRF, zip slip, and weak crypto (MD5/SHA-1, ECB, static IV) in fs, child_process, vm, and crypto | docs | |
eslint-plugin-secure-coding |
28 | Detects LDAP, XPath, XXE, GraphQL and template injection, unsafe deserialization, ReDoS, missing authentication, and PII in logs | docs | |
eslint-plugin-vercel-ai-security |
19 | Detects prompt injection, system-prompt leaks, hardcoded API keys, and unvalidated model output in generateText and streamText | docs | |
eslint-plugin-mongodb-security |
16 | Detects NoSQL operator injection, unsafe queries and regex, hardcoded connection strings, and missing TLS | docs | |
eslint-plugin-jwt-security |
13 | Detects algorithm confusion (CVE-2022-23540), alg:none, weak or hardcoded secrets, and decode-without-verify | docs | |
eslint-plugin-postgresql-security |
13 | Detects SQL injection, unreleased clients, floating queries, unsafe search_path, and insecure SSL | docs | |
eslint-plugin-knex-security |
5 | Detects SQL injection in raw queries built with string concatenation or template literals, connection configuration that disables TLS or certificate validation, and hardcoded database credentials | docs | |
eslint-plugin-drizzle-security |
4 | Detects SQL injection in raw queries built with string concatenation or template literals | docs | |
eslint-plugin-mcp-sdk-security |
4 | Catches tools registered without an input schema, handlers reading arguments the schema never declared, model-visible descriptions built from dynamic text, and tool arguments reaching a shell | readme | |
eslint-plugin-prisma-security |
4 | Detects SQL injection in raw queries built with string concatenation or template literals | docs | |
eslint-plugin-sequelize-security |
4 | Detects SQL injection in raw sequelize.query() and Sequelize.literal() calls built with string concatenation or template literals, connection configuration that disables TLS or certificate validation, and hardcoded database credentials | docs | |
eslint-plugin-typeorm-security |
4 | Detects SQL injection in raw queries built with string concatenation or template literals, connection configuration that disables TLS or certificate validation, and hardcoded database credentials | docs | |
eslint-plugin-anthropic-security |
3 | Catches hardcoded Claude API keys, the browser escape hatch that ships them to every visitor, and system prompts assembled from untrusted input | readme | |
eslint-plugin-gemini-security |
3 | Catches safety thresholds set to BLOCK_NONE, hardcoded API keys, and system instructions assembled from untrusted input | readme | |
eslint-plugin-mysql-security |
3 | Detects SQL injection in raw queries built with string concatenation or template literals, connection configuration that disables TLS or certificate validation, and hardcoded database credentials | docs | |
eslint-plugin-openai-security |
3 | Catches dangerouslyAllowBrowser, hardcoded API keys, and system prompts assembled from untrusted input | readme | |
eslint-plugin-sqlite-security |
1 | Detects SQL injection in raw queries built with string concatenation or template literals | docs |
🏗️ Security — application frameworks
3 plugins · 51 rules
| Package | Rules | What it catches | Docs | Downloads |
|---|---|---|---|---|
eslint-plugin-express-security |
28 | Detects permissive CORS, missing CSRF protection, missing helmet headers, insecure cookies, and GraphQL introspection in production | docs | |
eslint-plugin-lambda-security |
14 | Detects overly permissive IAM policies and CORS, unvalidated event bodies, secrets in env vars, and leaked error details | docs | |
eslint-plugin-nestjs-security |
9 | Detects missing auth guards, missing validation pipes, unthrottled routes, and exposed private fields | docs |
🧭 Architecture
1 plugin · 55 rules
| Package | Rules | What it catches | Docs | Downloads |
|---|---|---|---|---|
eslint-plugin-import-next |
55 | Drop-in replacement for eslint-plugin-import, 3.1x faster end-to-end, zero-config migration | docs |
✨ Code quality
6 plugins · 52 rules
| Package | Rules | What it catches | Docs | Downloads |
|---|---|---|---|---|
eslint-plugin-conventions |
15 | Enforces filename case, magic-number bans, commented-out code, expiring TODOs, and deprecated-API usage | docs | |
eslint-plugin-maintainability |
12 | Limits cognitive complexity, nesting depth, parameter counts, duplicate functions, and unhandled or silent errors | docs | |
eslint-plugin-reliability |
9 | Enforces error handling, network timeouts, null checks, and safe type narrowing | docs | |
eslint-plugin-modularity |
6 | Enforces DDD value objects and anemic-model checks, naming, REST conventions, and utility isolation | docs | |
eslint-plugin-operability |
6 | Bans debug code and console logging in production, verbose error messages, and process.exit calls | docs | |
eslint-plugin-modernization |
4 | Auto-fixes legacy patterns to ES2022+ (Array.at, template literals, EventTarget, Array.isArray) | docs |
⚛️ React
2 plugins · 98 rules
| Package | Rules | What it catches | Docs | Downloads |
|---|---|---|---|---|
eslint-plugin-react-features |
61 | Hooks, prop-types, JSX correctness, render performance, and class-to-hooks migration rules for modern React codebases | docs | |
eslint-plugin-react-a11y |
37 | WCAG 2.1 rules for ARIA, alt text, keyboard interaction, and focus management, with auto-fixes | docs |
Supporting tools
| Package | What it does |
|---|---|
@interlace/eslint-devkit |
Build your own team-specific rules — AST helpers, typed rule creator, and the LLM message formatter these plugins use |
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.
| What that buys you | |
|---|---|
| 🚀 No upstream queue | A CVE lands, a rule ships. We measure that latency ourselves — cve-rule-latency.md |
| 🤖 AI-optimized messages | CWE + CVSS + OWASP + compliance tags inside the message body, where the model actually reads them |
| ⚡ One codebase | Shared AST utilities, shared perf budget, one per-rule latency gate across every rule |
| 🏗️ Consistent surface | Same preset names, same option shapes, same docs layout in every plugin |
| 📚 Educational by default | Each message explains why, not just what — the guardrail teaches on the way past |
| 🔧 Modern flat config | ESLint 8.40 / 9 / 10, plus an oxlint parity gate for the rules oxlint can run natively |
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:
| Code IS vulnerable | Code is clean | |
|---|---|---|
| Rule fires | TP — signal | FP — noise |
| Rule silent | FN — miss | TN — correct quiet |
- 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.
Recall first, precision second. A missed CWE is worse than a noisy rule, and we don't regress recall to chase FPs.
On the ILB-Arena corpus (40 vulnerable / 38 safe fixtures, 18 plugins scored, 17 security-relevant) Interlace ranks 1st; the next-best plugin scores 66.1% F1. We publish the ordinal result and not the absolute one on purpose — a perfect score on a self-authored 40-fixture corpus is a regression test wearing a benchmark's clothes, and we withdrew our own headline number when an audit found it unsupported by its own evidence file.
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
| Stack | Cold | Warm | Findings | Files |
|---|---|---|---|---|
| Interlace on ESLint | ||||
| Interlace on oxlint | ||||
| Community plugins (ESLint) | ||||
| oxlint built-ins (different scope) |
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.
| Job | Result |
|---|---|
| Circular dependencies | |
| DOM XSS sinks | |
| Hardcoded secrets | |
| Command / shell injection | |
| ReDoS | |
| Path traversal | |
| Timing-attack comparison |
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)
| ESLint major | Weekly downloads | Share | Status |
|---|---|---|---|
| v10 | 23.6M | 11.08% | ✅ Supported (forward-looking) |
| v9 | 109.1M | 51.13% | ✅ Supported (current default) |
| v8 (≥ 8.40) | 60.3M | 28.29% | ✅ Supported (legacy active) |
| v7 and older | 20.3M | 9.51% | ❌ Unsupported (EOL) |
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
| Node.js | Status |
|---|---|
| 24.x | ✅ Active development — the repo's engines.node pin, what CI runs |
| 22.x LTS | ✅ Supported — recommended for production |
| 20.x LTS | ✅ Supported — long-term-stable baseline |
| 18.x | ✅ Supported (minimum) — every package's engines.node: ">=18.0.0" |
| ≤ 17 | ❌ Unsupported (EOL upstream) |
Who this is for
| Role | What it does for you |
|---|---|
| Security engineers | Catch CWEs at edit time with CVSS + OWASP already attached, and export SARIF into GitHub code scanning |
| Tech leads | Enforce architectural decisions automatically instead of re-litigating them in review |
| Platform teams | One guardrail set that scales across repos, with a per-rule latency budget so CI doesn't pay for it |
| Teams shipping with AI | Messages an LLM can act on correctly — the difference between "fixed the lint error" and "fixed the vulnerability" |
| Engineering managers | New engineers learn the codebase through guardrails, not tribal knowledge |
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.
Links
| 📚 Docs | eslint.interlace.tools |
| 📦 npm | All Interlace packages |
| 📊 Live metrics | ofriperetz.dev/stats |
| ✍️ Writeups | dev.to/ofri-peretz |
License
MIT © Ofri Peretz — see LICENSE.
Made with ❤️ from lessons learned in the trenches