Free CLI script - MIT - no dependencies
Lint Coverage Audit
A green lint step is not evidence that anything was linted. Seven checks, run against your repo, for the ways ESLint reports success while covering less than you assume.
Run it
curl -fsSL https://codingdunia.com/lint-coverage-audit.mjs -o lint-coverage-audit.mjs && node lint-coverage-audit.mjs Run it from the directory that owns your ESLint config, or pass a path: node lint-coverage-audit.mjs packages/web. Add --json for machine-readable output. It exits 0 either way, because it reports rather than gates.
Reading the source before running it is the right instinct. It is one file, a bit over 400 lines, and here it is.
What it reports
-
Scope
Counts the files ESLint actually opened against the lintable files on disk, and groups the gap by directory.
Ignore patterns, a files glob that misses an extension, and a narrow CI invocation all shrink the real count silently. The number in the JSON output is the only honest one.
-
Fatal parse errors
Lists every file ESLint could not parse.
A file that fails to parse is a file where no rule ran at all. The usual cause is parserOptions.project pointing at a tsconfig that does not cover the directory.
-
Active rules
Runs --print-config on a real file from your repo and separates configured rules from active ones.
A rule set to off is configured, not running. The two counts are usually far apart, and the gap is where upgrades get paid for.
-
Ghost plugins
Finds plugin prefixes your config references, then probes whether the plugin is installed at all.
off is the only severity ESLint never resolves, so a config can name plugins that do not exist, indefinitely, without a word. The probe sets one rule to error and reads the failure.
-
Config generator leftovers
Reads the resolved languageOptions and flags two specific shapes: parserOptions.project set while no type-aware rule is active, and ecmaVersion 5 or sourceType script.
The first makes you pay for a full TypeScript program on every run that no rule consumes. The second is what migration generators write and nobody re-reads; at ecmaVersion 5 modern globals are absent, which surfaces as no-undef on valid code.
-
Orphan disable comments
Counts eslint-disable comments and reports how many sit in files ESLint never reads.
--report-unused-disable-directives only reports on linted files, so it is blind exactly where unnecessary disables collect.
-
CI gate
Scans your CI definitions for a lint invocation and checks whether its exit code can propagate.
In sh, a script exits with the status of its last command. A lint call followed by a copy or an upload reports the copy.
What the output looks like
This is a real run against one workspace of the monorepo behind this site, trimmed to fit. The first finding is the one worth internalising: twenty rule entries pointing at a plugin that was never installed, every one of them set to off, so ESLint had never said a word about it.
lint-coverage-audit -- what your ESLint run is not checking
config: eslint.config.ts (flat)
[!] 1 plugin referenced in the config but not installed
These rules are all set to off, which is the only severity ESLint
never resolves. The config reads like policy; nothing backs it.
- @typescript-eslint/* -- 20 rule entries, plugin absent
[!] 2 of 8 eslint-disable comments (25%) sit in files ESLint never reads
--report-unused-disable-directives only reports on linted files.
- 2 in src/pages/search.astro
[~] ESLint opened 195 of 220 lintable files (89%)
25 files were never opened.
- 15 in src/pages/
- 9 in src/components/
[ok] 64 active rules of 423 configured
[ok] CI lint invocation looks gated
2 needing attention, 1 worth a look. Nothing was changed. Eighteen of those twenty entries came from a shared config that switches off formatting rules across plugins, including rules from a plugin this workspace does not have. Nothing in that chain is a bug in any single package. It is just what off does.
Where this came from
Two repos, audited by hand, and the same shapes kept coming back. The write-ups have the full measurements: four ways ESLint reports success while checking nothing covers the CI gate, the scope gap and the off severity, and the flat config migration guide covers the parser-without-plugin trap that started it.
The script exists because those numbers are from private repos and you have no reason to take my word for them. Run it on yours instead.
Questions
Does the script change anything in my repo?
No. It only reads files and runs your own ESLint in read-only modes: --format json, --print-config, and a single --rule probe against one file. It writes nothing, installs nothing, and has no network calls, so the only way to see its output is on your own terminal.
Does it need any dependencies?
None beyond what you already have. It is a single .mjs file using Node built-ins only, and it calls the ESLint binary already installed in your repo. If there is no local install it falls back to npx.
Will it work on legacy .eslintrc config, not just flat config?
Yes. It detects either format and reports which one it found. The checks that matter most, scope and ghost plugins, behave the same way under both, because both formats let a rule set to off reference a plugin that is not installed.
Why does it report files as unopened that I deliberately ignore?
Because the point is to make the number visible rather than assumed. Deliberate ignores and accidental gaps look identical from the outside, so the script shows the whole gap grouped by directory and leaves the judgement to you. A test directory you excluded on purpose is fine; a scripts directory nobody noticed is not.
Is a clean report proof my linting is fine?
No. It proves that the seven specific coverage failures it knows about are absent. It says nothing about whether your rule set is the right one, or whether the rules you do run catch the bugs your codebase actually produces.
Can I run it in CI?
Yes, and --json gives machine-readable output for that. It always exits 0 by design, because it is a diagnostic and not a gate. If you want it to block, read the findings array and decide which levels matter to you.