DEV Community

Cover image for Enable That Strict ESLint Rule Today: Bulk-Suppress the Backlog, Let Agents Burn It Down
Gabriel Anhaia
Gabriel Anhaia

Posted on Edited on AI-assisted

Enable That Strict ESLint Rule Today: Bulk-Suppress the Backlog, Let Agents Burn It Down


You know the line. It sits in eslint.config.mjs with a comment above it: // TODO: turn back on. Somebody tried to enable @typescript-eslint/no-explicit-any once, the run printed a few hundred errors across the whole repo, and nobody was going to review a PR that touched every folder. So the rule stayed off, and every week new any types landed on top of the old ones.

ESLint fixed this problem in v9.24.0, released April 4, 2025, with a feature called bulk suppressions. You set the rule to "error", record the violations that already exist in a file, and commit that file. From then on, the rule is enforced for new code and the old violations stay quiet until someone fixes them.

Coding agents can do the fixing. Split the backlog by directory and you get a list of small tasks, each with a pass/fail check, which is the kind of work agents finish.

Step 1: flip the rule to error and record the backlog

Bulk suppressions only apply to rules set to "error". Rules at "warn" are never suppressed. So the first change is the config. This is the typescript-eslint getting-started config with the rule switched on:

// eslint.config.mjs
import js from "@eslint/js";
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";

export default defineConfig({
  files: ["**/*.{js,ts}"],
  extends: [
    js.configs.recommended,
    tseslint.configs.recommended,
  ],
  rules: {
    "@typescript-eslint/no-explicit-any": "error",
  },
});
Enter fullscreen mode Exit fullscreen mode

Then record what is already there:

npx eslint . --suppress-rule @typescript-eslint/no-explicit-any --fix
Enter fullscreen mode Exit fullscreen mode

The ESLint docs recommend --fix here so anything auto-fixable gets fixed instead of suppressed. no-explicit-any is not auto-fixable with its default options, so nothing changes for this rule. But --fix applies to every fixable rule in your config, so run it on a clean working tree and read the diff before you commit. --suppress-rule can be repeated for several rules, and --suppress-all suppresses every rule at "error".

ESLint writes eslint-suppressions.json in the project root. The format is a file path, then a rule, then a count. An illustrative excerpt (the paths and counts are made up):

{
  "src/billing/invoice.ts": {
    "@typescript-eslint/no-explicit-any": {
      "count": 4
    }
  },
  "src/users/profile.ts": {
    "@typescript-eslint/no-explicit-any": {
      "count": 2
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Paths are relative to the working directory, in POSIX form, and keys are sorted since v9.27.0, which keeps the diffs small. Commit the file. If you want it somewhere else, --suppressions-location moves it, but then you have to pass that flag on every run, not only when creating it.

What CI enforces from now on

Your CI step does not change. It is still npx eslint .. What changes is what makes it fail.

A new any in a file with no suppressions fails the build, exactly as before. A new any in a file that already has suppressed ones also fails, and louder than you might expect. Suppressions are counted per file and per rule, not per line, so ESLint cannot tell which of the five violations is the new one. The bulk suppressions announcement says it plainly: "ESLint will report all of them." The source does the same thing: a rule is suppressed in a file only while the current count is at or below the stored count.

Fix one suppressed violation and its entry becomes unused. By default ESLint then exits with code 2 and reports the unused suppressions. The fix is one command:

npx eslint . --prune-suppressions
Enter fullscreen mode Exit fullscreen mode

So a fix that lands without a pruned file fails CI, and a new violation fails CI. That is the ratchet you wanted when you wrote the TODO. It has one gap: anyone can re-run --suppress-rule in a PR and commit a bigger file, and CI will pass. Treat any line added to eslint-suppressions.json as a review flag.

A ratchet wheel turning one way, with a stack of old error tokens shrinking beside it
Fixes shrink the file; new violations fail the build.

There is an escape hatch, --pass-on-unpruned-suppressions, added in v9.28.0, which stops unused suppressions from affecting the exit code. It is fine for a local run. In CI it lets fixed entries linger in the file, which means the file stops telling you how much backlog is left. Leave it off there.

One more limit: creating and pruning suppressions is CLI-only. The Node.js API can apply existing suppressions with applySuppressions: true, which arrived in v10.1.0. Whether your editor stops underlining suppressed errors depends on whether its ESLint integration turns that option on.

Step 2: size the backlog per directory with jq

Before you hand anything out, find out where the violations live. With jq, the total for one rule:

RULE=@typescript-eslint/no-explicit-any
jq --arg r "$RULE" \
  '[.[] | .[$r].count // empty] | add' \
  eslint-suppressions.json
Enter fullscreen mode Exit fullscreen mode

And the same count grouped by the first two path segments (src/billing, src/users), biggest first:

jq -r --arg r "$RULE" '
  [ to_entries[]
    | select(.value[$r])
    | { dir: (.key | split("/") | .[0:2] | join("/")),
        n: .value[$r].count } ]
  | group_by(.dir)
  | map("\(map(.n) | add)\t\(.[0].dir)")
  | .[]' eslint-suppressions.json | sort -rn
Enter fullscreen mode Exit fullscreen mode

Each line is a count and a directory. That list is your task list. If one directory is much bigger than the rest, change .[0:2] to .[0:3] for that subtree and split it further. A task an agent can finish in one sitting, and a human can review in one sitting, is the size you want.

Step 3: one directory per agent task

Each task gets the same brief with a different path. Write it so the agent has a clear finish line and no room to cheat:

Fix every @typescript-eslint/no-explicit-any error
in src/billing. Use real types, or unknown plus a
type guard. Do not add eslint-disable comments and
do not add casts whose only job is to silence the
rule. Do not edit files outside src/billing.
Keep runtime behavior the same; run the tests.
When done, run `npx eslint . --prune-suppressions`,
then `npx eslint .` must pass. Commit the code and
eslint-suppressions.json together.
Enter fullscreen mode Exit fullscreen mode

Pruning in one branch won't touch the other directories' entries. --prune-suppressions removes suppressions that no longer match a violation, plus entries for deleted files, and nothing else.

Several agents on one checkout will trip over each other, so each task needs its own branch and folder. In plain git that is one worktree per directory:

git worktree add -b lint/any-billing ../app-any-billing
git worktree add -b lint/any-users ../app-any-users
Enter fullscreen mode Exit fullscreen mode

I build Hermes IDE, so weigh this part with that in mind. In Hermes, ⌘N (Ctrl+Shift+N on Windows and Linux) starts a task on its own hermes/ branch in its own worktree, with the task text as the agent's first prompt. Paste the brief, change the path, pick Claude Code or Codex or another agent, and launch. Then do the next directory.

The Hermes New task sheet with a task typed in, chips for Claude Code, the project, a new hermes/ worktree, Accept edits, model and effort, and a preview of the command Hermes will run
The ⌘N New task sheet: agent, project, a new hermes/ worktree, approval mode, model and effort, and the "Hermes will run" preview of the claude command.

A worktree recipe makes each new worktree ready to lint. Hermes reads .hermes/worktree.toml from your repo: setup runs in each new worktree, and done_when sets the default checks that decide when a task is done. For this job the check is the lint run itself:

# .hermes/worktree.toml
setup = ["npm ci"]
done_when = ["npx eslint ."]
Enter fullscreen mode Exit fullscreen mode

The first time Hermes sees the file, and whenever it changes, it shows you the setup commands and waits for you to choose Run setup.

Step 4: review the code and the shrinking file together

When a task finishes, look at two things side by side: the code change, and the lines removed from eslint-suppressions.json. The count that went away should match the fixes you see. If entries for src/users vanished from a src/billing task, something went wrong.

In Hermes, ⌘G (Ctrl+Shift+G) opens the Review Desk, which shows changes by turn or by file. The suppressions file appears in the list next to the source files, so you can read the removed entries against the code that removed them. A line comment there goes back to the agent that wrote the line.

The Hermes Review Desk in By turn view, listing two turns with their file and line counts, a diff of one source file, the changed-files list, and a commit message drafted from the turns
The Review Desk (⌘G) in By turn view: two turns, turn 1's diff of src/db.ts, the changed-files list and a commit message drafted from the turns.

The rule itself catches a new any. It does not catch the shortcuts around it, so check those by hand. A quick scan for new disable comments on the branch:

git diff main...HEAD | grep '^+.*eslint-disable'
Enter fullscreen mode Exit fullscreen mode

No output means no new disable comments. Also read every as SomeType the agent added. A cast with no check behind it moves the any problem somewhere the linter can't see.

When it looks right, land it. In Hermes, Land commits, opens a pull request or merges locally into the branch the task started from, and can be undone. In plain git it is your usual PR.

If two lint branches both edited eslint-suppressions.json and git reports a conflict in it, don't merge the JSON by hand. Each side only removed entries, so either version still lists every violation that is left. Take one, then run npx eslint . --prune-suppressions on the merged code and commit the result.

When this is the wrong tool

Bulk suppressions are a migration tool for errors you intend to fix. If the team doesn't agree the rule is worth it, suppressing hundreds of violations just hides an argument you haven't had yet. And because counts are per file, a file with many suppressed errors gets loud the moment anyone adds one more, which is the point, but warn people before it happens to them in the middle of an unrelated PR.

You also don't need agents for any of this. Steps 1 and 2 are plain ESLint and jq, and a person working down the directory list gets the same ratchet. Agents just make the list shorter faster, and the worktree per directory keeps their changes small enough to review.


If this was useful

The step that decides whether agents fix the backlog or paper over it is the brief in step 3: a narrow scope, a check that proves it's done, and the shortcuts ruled out up front. That is prompt engineering in its plainest form, and my Prompt Engineering Pocket Guide covers how to write instructions an LLM can't wriggle out of. If you want the one-task-per-worktree setup without the bookkeeping, Hermes IDE is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux; the code is on GitHub.

Prompt Engineering Pocket Guide: Techniques for Getting the Most from LLMs

Top comments (0)