We Pointed Anti-Slop at Our Own TypeScript and 21 of 53 Hits Landed in One File
Anti-slop is at 4,037 stars 22 days after its first commit. We vendored it, ran it across 7,180 lines of our own TypeScript, and got 53 findings, 21 of them in the single file we already trusted least.
The WJS Desk
Sep 4, 2026 · 5 min read

Anti-slop showed up on August 12 and is now at 4,037 stars, which is 183 a day since first commit. Our own snapshots put the current rate a lot lower: we recorded 3,914 stars on August 31 and 4,037 on September 3, so 123 stars in 79 hours, about 37 a day. The launch spike is over and it is holding at roughly a fifth of its lifetime average.
The pitch is one line: opinionated Oxlint rules that reject low-evidence TypeScript and JavaScript patterns. In practice "low-evidence" means code that asserts something the type system has not been shown. We were skeptical that this could be more than a vibe check, so we ran it against our own repository.
What it actually is
Fifteen generic rules, plus one opt-in rule for projects using Effect. The whole thing is 2,778 lines of TypeScript outside tests, with another 814 lines of tests, and exactly one runtime dependency: @oxlint/plugins, pinned to the same exact version as Oxlint itself. No AI, no model calls, no heuristics about prose. It is an AST plugin.
The rules that fired on us are the interesting ones:
anti-slop/require-safety-comment-for-type-assertion
anti-slop/no-unsafe-dictionary-type
anti-slop/no-known-value-widening
anti-slop/no-runtime-typeof
anti-slop/no-conditional-empty-object-spread
Installation is not an npm install, which we will come back to. You copy src/ into your repo, install @oxlint/plugins at the resolved Oxlint version, and register the entry point in oxlint.config.ts. There is also a bundled agent skill that does the copy and config for you.
We ran it on this site
We vendored the plugin into a scratch directory, copied in every hand-written .ts and .tsx file from apps/ and packages/, and turned all 15 rules on as errors. That is 90 files and 7,180 lines. Generated .next output was excluded, which matters: our first run drowned in machine-written Next.js type validators, and those numbers would have been meaningless.
| Measure | Oxlint, native rules only | Oxlint plus anti-slop |
|---|---|---|
| Findings on 90 files | 4 | 57 |
| Wall time, best of 5 | 0.06s | 0.23s |
| Runtime dependencies added | 0 | 1, version-pinned to Oxlint |
Four times slower is the honest cost of a JS plugin sitting on a Rust linter, and 0.23 seconds on 90 files is still nothing. On a repo 50 times this size it stops being nothing, and that ratio is the number to plan against rather than the absolute.
The distribution of the 53 anti-slop findings is where it got useful:
| Rule | Hits |
|---|---|
| require-safety-comment-for-type-assertion | 37 |
| no-unsafe-dictionary-type | 5 |
| no-known-value-widening | 5 |
| no-runtime-typeof | 4 |
| no-conditional-empty-object-spread | 2 |
| The other 10 rules | 0 |
Two thirds of the output is one rule. And 21 of the 53 landed in a single file, apps/web/lib/articles.ts, which is our Supabase data access layer. That is the file we would have nominated ourselves as the weakest in the repo, and the linter found it without being told.
Which hits were real
We read every finding rather than counting them. They fall into three groups.
Right, and worth fixing. Our data layer is full of lines like return (data ?? []) as Article[]: a Supabase response cast straight to a typed array with nothing validating the shape. If a query changes, the cast lies and TypeScript helps us believe it. Same for row.tags as Tag | null. That is exactly the pattern the rule exists to catch, and it caught ours.
Right, and cheap. no-known-value-widening flagged const STATUS_COLOR: Record<string, string> in the admin. Annotating a closed lookup as open-keyed throws away the compiler's ability to tell us a status is missing. Switching to satisfies is a one-line fix with a real payoff.
Wrong, or at least taste. It flagged our theme toggle for asserting localStorage.getItem(KEY) as Theme | null. The very next line is if (saved === 'light' || saved === 'dark' || saved === 'system'), a real runtime guard. The invariant is established; the rule wants a SAFETY: comment saying so. That is a documentation policy, not a safety finding, and a lot of the 37 are this shape. It also flagged the standard Next.js metadata idiom ...(explicitImage ? { images: [explicitImage] } : {}) twice, where the suggested rewrite is longer with no gain.
What it does not do
- It does not detect AI-written code. Nothing in the 2,778 lines looks at provenance. It matches patterns that correlate with unchecked generation, and it flags a human writing the same pattern identically.
- It has no autofix. Every one of our 53 findings is a manual edit.
- Ten of the fifteen rules never fired, because they target patterns we do not use:
Reflect.get, module mocking, chained assertions, unknown parameters and returns. On a codebase without those, you are buying five rules. - It is Oxlint only. No ESLint build, and
@oxlint/pluginsmust match your Oxlint version exactly, so the two upgrade together or not at all. - It is v0.1.2. Three tags in three weeks.
Who made it, and the trap on the way in
Bus factor one. Dillon Mulroy has 25 commits; one other contributor, dajiaohuang, has landed a single commit adding test coverage. Eight watchers against 4,037 stars is a wide gap, and it is the usual shape for a repo that got shared widely and adopted narrowly.
The maintainer is unusually direct about the model: "This project is meant to be vendored, not treated as a fixed npm dependency. There is no official npm package." The README says the ruleset reflects his own preferences rather than a universal standard. For a 2,778-line MIT plugin that is the right call, and it makes the bus factor mostly irrelevant. Vendored code does not go unmaintained, it just becomes yours.
Check what you are installing. The name oxlint-plugin-anti-slop does exist on npm. It was registered on August 12 2026, the same day the repo was created, by an account unrelated to the project, and it is version 0.0.0. We downloaded it: the tarball contains one file, a package.json with a name and a version and nothing else. No code, no repository field. It is not the linter, and following the README means never typing that command in the first place.
Verdict
Adopt it today if you have a data access layer that casts external payloads into your domain types, because that is what it is genuinely good at finding and it found ours in one pass. Turn on no-known-value-widening, no-unsafe-dictionary-type and require-safety-comment-for-type-assertion, and expect the third one to generate most of your work.
Wait if you were hoping to point it at a pile of generated code and get a slop score. It does not do that, and the 37 assertion findings on our repo were mostly written by humans over several months.
What would change our mind: an autofix that inserts a SAFETY: stub at each assertion site would turn the noisiest rule from a 37-item chore into a review prompt, and would make the whole ruleset adoptable on a large codebase in an afternoon instead of a sprint.


