The Linter Is the Cheapest Adult in the Room
There’s a principle worth tattooing on every agent harness: prompt for judgment, script the mechanical, gate the consequential, isolate the dangerous (constraint layering). Most teams get the first and last parts. The middle — script the mechanical — is where autonomous coding loops quietly bleed quality, because the mechanical layer everyone inherited was built for a different era.
ESLint was designed for humans who run it occasionally and read its warnings over coffee. An autonomous agent editing code in a loop is a different customer entirely. It edits every few seconds, it never reads warnings, and it treats exit code 0 as permission to proceed. Serve that customer a 45-second linter with a warning tier and you’ve built a gate that neither slows the agent down nor stops it — the worst of both.
The engine: Oxlint
Oxlint is a JavaScript/TypeScript linter rebuilt in Rust on the Oxc compiler stack. The published benchmarks claim 50–100× over ESLint, and the numbers matter less than the category change: linting drops from “CI stage you wait on” to “keystroke-speed feedback inside the agent’s inner loop.”
Three architectural choices make it agent-native rather than merely fast:
- Type-aware linting via
tsgolint— built ontypescript-go, so checks like floating-promise detection use the real TypeScript type system, not a reimplementation. One type system, one source of truth. - Multi-file analysis — a project-wide module graph shared across rules, which kills the classic
import/no-cycleperformance cliff. - Structured diagnostics — precise spans, contextual data, and documentation links, explicitly designed so that machine consumers can locate and repair issues reliably. A diagnostic an agent can act on is worth ten prose warnings.
The policy: Error, Never Warn
An engine needs a rulebook. @nkzw/oxlint-config — Christoph Nakazawa’s opinionated preset — is the cleanest public statement of what a lint policy for autonomous loops should look like. Its README says the quiet part out loud: “Warnings are noise and get ignored. Either it’s an issue, or it isn’t.”
Every principle in that config maps onto a documented agent failure mode from agentic code quality:
| Config principle | Failure mode it closes |
|---|---|
| Error, never warn | Agents ignore anything that doesn’t block; warnings train the loop to proceed |
Ban test.only | The reward-hack where a “green” suite silently skipped 99% of its tests |
Ban console.log | Debug scaffolding leaking into production diffs |
Ban instanceof | Plausible-looking code that breaks across bundle and realm boundaries |
| Prefer autofixable rules | Repair stays in the inner loop instead of burning review cycles |
| Deterministic sorting | Merge conflicts between parallel agent worktrees vanish at the syntax level |
That last row is underrated. When multiple agents (or an agent and a human) touch the same file, alphabetically sorted object keys, interface members, and JSX props mean diffs stay minimal and collisions stop being a coordination tax. Sorting isn’t aesthetics — it’s multi-agent merge-conflict prevention.
Examples: what the gate actually catches
Reward hacking via focused tests — the linter refuses the shortcut:
// ❌ Blocked: no-only-tests
it.only('passes the one test I did not break', () => { ... });
Hallucinated logic that reads fine and cannot work — caught at AST speed:
// ❌ Blocked: oxc/const-comparisons — x cannot be both
if (x > 10 && x < 5) { retry(); }
// ❌ Blocked: oxc/bad-min-max-func — clamp is inverted
const clamped = Math.min(0, Math.max(100, score));
Cross-realm booby traps — banned as a category:
// ❌ Blocked: @nkzw/no-instanceof — fails across iframes/duplicate deps
if (err instanceof CustomError) { ... }
// ✅ Brand check survives bundle boundaries
if (err?.name === 'CustomError') { ... }
Debug residue — the diff never ships:
// ❌ Blocked: no-console
console.log('payload', data);
// ❌ Blocked: no-warning-comments — @nocommit markers fail the build
// @nocommit temporary hack
Layering discipline, demonstrated in miniature
The most instructive part of the nkzw config is what it turns off. For TypeScript files, it disables no-undef, no-dupe-keys, no-unreachable, and a dozen other rules — because the type-checker already enforces them. Each invariant lives in exactly one layer, in the cheapest layer that can hold it.
That’s constraint layering applied to the toolchain itself. Don’t lint what the compiler checks. Don’t prompt what the linter enforces. Don’t burn a single token of agent attention asking the model to “remember” a rule a Rust binary verifies in milliseconds.
flowchart TD
E[Agent edit] --> L["oxlint: error-only, ms-scale"]
L -->|structured diagnostic| E
L --> T["tsc --noEmit: type contracts"]
T -->|fail: bounded repair| E
T --> B["Tests, architecture contracts, review"]
B --> R["Releasable patch"]
The deterministic lint gate is the first wall a patch hits and the cheapest one to bounce off. Every defect it absorbs at millisecond cost is a defect that would otherwise surface in a test run, a review pass, or production — each an order of magnitude more expensive than the last. That’s the whole economics of the releasable patch rate in one sentence: push rejection as early and as cheap as it will go.
The linter can’t judge your architecture, your intent, or your taste. But it is the only reviewer that reads every line of every diff in milliseconds, never gets tired, and never lets “it’s just a warning” slide. In a software factory, that makes it the cheapest adult in the room.
Grounded in deterministic-lint-gates, agentic-code-quality, and constraint-layering — compiled from the Oxc project documentation and @nkzw/oxlint-config v2.0.0.