Skip to main content
BHPS, home

Writing

AI slop and your codebase: Why you need a linter and a formatter more than ever

Jan Szotkowski

If you do not mind paying developers to wait on a terminal, paying for CI minutes on a pipeline that runs half an hour longer than it has to, and paying for tokens while an AI agent fixes the same mistake for the third time, do not read this article. You are not missing anything. Money and time clearly do not matter to you.

Everyone else, keep reading.

Today AI can write a component for you in a minute. Twenty components in five minutes. It works, the tests pass, everyone is happy. Then you open the pull request and see that one component uses interface, another uses type, a third has a forgotten console.log in the middle, and a fourth imports the same library in three different ways.

None of those things is a bug. The code runs. But a codebase where every file looks like it was written by someone else is hard to read, hard to review and hard to maintain. That flood of "it works, but nobody made it consistent" is what we now call AI slop. And every hour you spend cleaning it up is an hour you pay for.

The good news: you can prevent it almost for free. The bad news: if you do it with slow tools, you only prevent it halfway, because people will start working around them.

And an important point up front: it is not only an AI problem.

It is not only about AI, it is just more visible

In my article about the Interface vs. Type rule I wrote about what happens when a team grows. Ten developers, ten slightly different habits. Someone brings interface over from Java, someone mixes both, and code review turns into a debate about whitespace instead of architecture.

An AI agent is basically ten more developers who have never met. It does not know your "unwritten rule". Every session starts from zero, the model has learned thousands of different styles and picks whichever fits the moment. On top of that it writes faster than any of us, so inconsistencies grow faster than anyone can catch them.

The answer is the same as before, only more urgent: the rules have to be enforced by a machine, not by human memory.

A rule in a prompt is a request, a rule in a linter is a law

Of course you can write into AGENTS.md or a system prompt: "Use type instead of interface, do not use any, do not leave console.log behind." It often works. But it is only a request. The model forgets it once the context fills up, or interprets it its own way.

A lint rule works differently. It either passes or it fails. There is no discussion.

Modern AI agents also work in a loop:

  1. write code,
  2. run checks (tests, types, lint),
  3. read the errors,
  4. fix them and go again.

A linter and a formatter are exactly the feedback this loop needs. The agent does not have to guess what you like. It runs one command and gets a concrete answer: line 42, this rule, this is not allowed.

A formatter also removes a whole category of noise. Neither the agent nor the human has to think about quotes, indentation or trailing commas. The diff in a pull request contains only real changes, and the reviewer focuses on what the code does, not on how it looks.

A short history: ESLint and Prettier

ESLint appeared in 2013 and Prettier in 2017. They changed the JavaScript world. ESLint guarded code quality (unused variables, dangerous constructs, custom team rules), and Prettier ended the eternal arguments about formatting by deciding for everyone.

The ESLint + Prettier pair became the standard and worked great for a decade. But it had one property we learned to live with: both tools are written in JavaScript and run on Node.js. On a small project nobody notices. On a large TypeScript codebase with type-aware rules, linting becomes something you run "at the very end" because it takes minutes.

Rust changed the rules of the game

Then came the Oxc project and with it Oxlint (2023) and later Oxfmt. Both are written in Rust, run natively and use every core of your CPU.

The difference in speed is not cosmetic. One developer on Reddit described migrating their company project (M3 MacBook Pro, median of three runs, over 6,000 files):

  • ESLint (single-threaded, type-aware rules): ~2 min 27 s
  • Oxlint (5,360 files, 134 rules, 11 threads): ~1.3 s
  • Prettier: ~13.9 s
  • Oxfmt: ~2.1 s

Mind that this is one specific project with one specific configuration, and your results will differ. The official numbers in the Oxc docs are more modest: Oxfmt is roughly 30x faster than Prettier. But the order of magnitude is the same everywhere. We are talking about tens and hundreds of times, not tens of percent.

And here is what has changed over the past year:

  • Oxfmt has been in beta since February 2026 and passes 100% of Prettier's conformance tests for JavaScript and TypeScript. The remaining differences are known bugs in Prettier itself. The output is not different, it is just faster.
  • Type-aware linting has been stable since July 2026. It is powered by tsgolint, built on native TypeScript 7, and covers 59 of the 61 type-aware rules from typescript-eslint. This was for a long time the biggest excuse for staying on ESLint.
  • Oxlint now supports more than 800 rules from ESLint core and popular plugins (React, jsx-a11y, import, unicorn...) and can run JS plugins too.

Speed is money

I always say this, even though it sounds a bit dry: tooling speed is a business argument.

Let us run an illustrative example. A team of ten developers, each running checks ten times a day (before a commit, before a push, while fixing things after CI). With two and a half minutes of waiting per run that is 10 × 10 × 2.5 min, roughly 4 hours a day and about 80 hours a month spent staring at a terminal. With Oxlint (1.3 s per run) it adds up to under two minutes a day.

And that is before we count:

  • CI/CD pipelines. Every minute in a pipeline is paid for (runners) and waited for (developers). Faster linting means faster feedback on a pull request and faster releases.
  • Pre-commit hooks. People start bypassing a hook that takes 40 seconds with --no-verify. Nobody disables a hook that takes a second.
  • Delivery. A faster cycle means features and bug fixes reach clients sooner. That is what the customer asks about.

Where AI fits in

This is where the speed multiplies. A developer runs lint a few times a day. An AI agent runs it after every iteration, easily twenty times per task.

If every run takes two minutes, the agent either waits (and you wait with it, or pay for cloud agent runtime), or, which is worse, starts skipping the check. Slow feedback means the agent does more work blindly and then fixes more things at once.

As for tokens, let us be precise: waiting itself does not burn tokens. What burns them is the number of iterations and the size of the context. When the agent gets fast, precise feedback right after every change, it fixes the problem while it is still small. When it gets feedback only at the end, it has to re-read a bigger chunk of code, untangle dependencies and fix things it has since built on a wrong foundation. Depending on the model you use, you can see that on the invoice.

The result: a fast linter is to an AI agent what fast tests are to a human. As long as it takes seconds, it gets used. Once it takes minutes, people start walking around it.

How we set it up

Let us not stay in theory. This website runs on Oxlint and Oxfmt. The linter config looks like this:

import { defineConfig } from 'oxlint';

export default defineConfig({
    plugins: ['import', 'unicorn', 'react', 'typescript', 'jsx-a11y'],
    categories: {
        correctness: 'warn',
    },
    rules: {
        'eslint/no-console': ['error', { allow: ['warn', 'error'] }],
        '@typescript-eslint/consistent-type-definitions': ['error', 'type'],
        '@typescript-eslint/no-explicit-any': 'error',
        '@typescript-eslint/no-unused-vars': 'error',
    },
});

Look at the middle line. The consistent-type-definitions rule, which I once wrote my own ESLint plugin for, is now one line of config and runs natively in Oxlint. The same goes for the accessibility rules (jsx-a11y) that AI loves to skip.

And so we do not have to think about three commands, there is one in package.json:

{
    "scripts": {
        "lint": "oxlint",
        "fmt": "oxfmt",
        "cq": "oxlint && oxfmt --check && tsc --noEmit"
    }
}

cq (code quality) runs lint, the format check and types. The same command runs in CI. The same command is what we tell the agent. One sentence in the agent instructions (AGENTS.md, CLAUDE.md, rules) is enough:

Before you call the work done, run `pnpm run cq` and fix every finding.

That turns a "recommendation" into a gate the code has to pass, whether a human or a model wrote it.

It is not 100% there yet (and I would still not reach for anything else)

I do not want to write an advert, so let me be straight: Oxlint and Oxfmt are not yet a 100% replacement for ESLint and Prettier. A few things you may run into:

  • Not every rule exists. Oxlint covers more than 800 rules, but some special rule from an ESLint plugin you depend on may be missing.
  • Custom ESLint plugins. Local plugins are not migrated automatically, but you can add them by hand to the config through jsPlugins. Oxlint supports most of the ESLint v9 API, not all of it.
  • Prettier plugins and less common formats. Oxfmt is in beta and Prettier plugin support is still catching up. If you depend on the exact behavior of a specific plugin, check it in advance.
  • Type-aware linting needs TypeScript 7. On an older version that is one more step.

So what do you do? My position is simple: until something better comes along, I would not reach for anything other than Oxlint and Oxfmt. Not because they are perfect, but because the speed difference is so large that a few missing rules or small deviations can be worked around, while a slow tool in your pipeline never can. Development is also moving fast: in a few months Oxfmt went from alpha to a beta with full compatibility on JS and TS, and type-aware linting became stable.

If you really miss a rule, you can bridge it by running Oxlint first and leaving ESLint only for what remains (oxlint && eslint, and the eslint-plugin-oxlint plugin turns off duplicates). Treat that as a temporary bridge, not the end state.

How to start (in an afternoon)

Migration is surprisingly painless today.

Linter:

pnpm add -D oxlint
npx @oxlint/migrate

Formatter:

pnpm add -D oxfmt@latest
pnpm oxfmt --migrate=prettier
pnpm oxfmt

Keep the bulk reformat as a separate commit and add its hash to .git-blame-ignore-revs so git blame does not point at "who reformatted the whole project". Then update CI and pre-commit hooks, and finally put the command into your AI agent instructions.

Conclusion

In a world where a model writes much of the code, consistency is not a luxury. It is the one thing that keeps a codebase readable for humans and for the next AI agents that come after you.

Rules have to be automatic (we do not want to think about them all the time), enforced (no "I will be more careful next time") and fast (otherwise everyone avoids them). ESLint and Prettier taught us that. Oxlint and Oxfmt took it to the point where a check takes a second, not minutes. And that pays off whether you are paying for developer time, CI minutes or tokens.

If lint is the step you run "at the very end" today, try switching it. It takes an afternoon. And then tell me how many seconds you got back.