agent-lint-guardrails
Rules too strict for a human are the right ceiling for an agent. An agent will happily write a
300-line function with a cyclomatic complexity of 40; a human reviewer will not catch it at 2am
across 40 repos. So move the ceiling into the linter, run it in CI, and let the agent self-correct
before the PR ever reaches you.
The budgets (oxlint)
Use oxlint (oxc, https://oxc.rs) — one fast binary, no plugin install, runs in CI and
pre-commit. Default agent budgets:
| Rule |
Cap |
Why |
complexity |
10 |
one function, one job — split, don't raise the cap |
max-lines |
300 |
a file over 300 lines is two files |
max-lines-per-function |
50 |
a function over 50 lines hides its own bugs |
max-params |
4 |
more than 4 params → pass an object |
max-depth |
4 |
deep nesting is un-reviewable |
max-nested-callbacks |
4 |
same |
| coverage |
ratchet |
every PR must not lower codecov; new code carries tests |
The canonical .oxlintrc.json + workflow live in ~/oxlint-rollout/ on the author's machine.
Ship it warn-first, then ratchet — the only way to fan out
Strict-as-error is a trap for an existing fleet. Measured on one real repo (offrouter, 702 TS
files): strict budgets produced ~470 blocking errors — 266 complexity, 121 long-function, 42
big-file, plus hundreds of correctness/perf category errors. Multiply across 40 repos and you
have a multi-week refactor campaign, not a rollout — and every PR red-CIs and blocks its own merge.
So: every rule ships as warn. CI stays green, the PR auto-merges, and the violation counts
become visible on every run. Then ratchet per repo over time — promote correctness to error
first (real bugs), then the size/complexity budgets as you burn them down. A simple counter beats
a grand plan: pick one repo, fix its top violations, flip those rules to error, move on.
Three gotchas that will bite you (all learned the hard way)
- oxlint has NO
no-restricted-syntax. The React "deslop" hook bans (below) CANNOT ride on
oxlint — the config fails to parse with Rule 'no-restricted-syntax' not found. Budgets live in
oxlint; the React bans live in eslint. Two linters, two jobs.
- Parse errors can't be downgraded by config. oxlint keeps a couple of parser-level errors
(
Empty parenthesized expression, missing-semicolon on some TSX) at error severity no matter
what categories say — so the job exits 1 even in an all-warn config. Fix: the CI step runs
bunx oxlint@^1 --config .oxlintrc.json || true during the warn-first phase. Drop the || true
when you ratchet that repo to enforcement.
- Most personal repos have "Allow auto-merge" OFF.
gh pr merge --auto then fails. Either the
PR is immediately mergeable (direct --squash) or it is BLOCKED by a required review — do NOT
admin-bypass a required review to land a lint config; that defeats the guardrail. Leave it for
the one-click approval.
React feature code — the deslop ban-list (eslint, warn-first)
In feature code, derive state; do not store it. Ban via eslint no-restricted-syntax
(CallExpression selectors) at warn, then flip to error:
- No
useState / useEffect / useLayoutEffect / useReducer / useSyncExternalStore — derive
from the backend + URL params; load data through a loader-initiated useQuery (tanstack router
preload="instant" makes the route instant).
- No
useMemo / useCallback — the React Compiler handles memoization.
- No overzealous destructuring; no type casting outside tests.
These are style, not correctness — an eslint-disable with a one-line reason is the rare escape
hatch, never the default fix.
Rollout mechanics (fleet-scale)
- Fan-out unit = 2 self-contained files (
.oxlintrc.json + a standalone oxlint.yml that runs
bunx oxlint on ubicloud-standard-4 — private pooriaarab/* repos run every job on an Ubicloud
runner, and the self-hosted Dell fleet is retired), independent of each repo's existing
turbo/eslint pipeline. Uniform, minimal blast radius, no per-repo bespoke wiring.
- No local clone needed — create the branch and both files through the
gh contents API
(the token needs workflow scope; a git push of a workflow file works, but the contents-API
edit of a workflow is classifier-gated, so patch workflows via clone+push).
- Skip forks and empty scaffolds.
isFork==true → never lint someone else's code. A 3–4 KB
repo is an empty scaffold — skip it.
- Deploy guard before merge. Check whether the PR's base branch has a deploy/release workflow
triggered by push to it. On the standard layout the default branch
main deploys to the staging
environment and only release deploys production, so a main merge is safe and a PR based on
release is not. Merge freely where the base deploys to staging or a demo; hold where merging it
deploys production. staging and production are environment names — never retarget to a branch
by those names, and never create one.
- Check the trigger against the branches that exist. A
branches: filter naming a branch the
repo does not have never fires and never errors, so a fanned-out workflow can land in dozens of
repos and run in none of them. Read git branch -r (or the repo's default branch from the API)
before you trust a green fan-out.
Beyond lint — the fuller agentic guardrail stack
Lint budgets are one layer. The complete stack for high-quality agent-authored code:
- Lint budgets (this skill) — complexity, size, deslop.
- Coverage ratchet — a CI gate that fails when a PR lowers coverage; new code carries tests.
- Asset/bundle byte budget — a size check on built JS/CSS so an agent can't 3× the bundle.
- Typecheck as a required check —
tsc --noEmit, no any (oxlint no-explicit-any).
- Secret + dependency scanning — gitleaks on every PR; Dependabot/
bun update cadence.
- A review gate — an LLM council or
claude-review that must approve, plus one human click on
protected branches. Do not let agents bypass it.
- CI economics — at agent PR volume, add concurrency-cancel + path filters (see the
ci-cost-at-agent-scale skill) before the bill surprises you.
Roll each out the same way: warn/advisory first across the fleet, then ratchet to blocking per repo.
Related
- pr-standards — the pull-request rule these budgets keep a PR inside: one issue, one concern, under 500 lines, with proof of the work.
1---2name: agent-lint-guardrails3description: Use when setting up or tightening automatic code-quality guardrails on a repo where agents open most of the pull requests — cyclomatic-complexity budgets, file/function size caps, the React 'deslop' hook bans, and a coverage ratchet. Rules that are draconian for a human are the right ceiling for an agent: they cap the blast radius of vibe-coded churn and keep diffs reviewable. Covers the oxlint config, the warn-first-then-ratchet rollout strategy that lets budgets ship green across a whole fleet at once, and the hard gotchas (oxlint has no no-restricted-syntax, parse errors can't be downgraded, most repos have Allow-auto-merge off). Triggers: 'add lint budgets', 'cyclomatic complexity limit', 'oxlint', 'deslop', 'ban useEffect/useState', 'coverage ratchet', 'lint rules for agents', 'stop the slop', 'roll lint to all my repos'.4---56# agent-lint-guardrails78Rules too strict for a human are the right ceiling for an agent. An agent will happily write a9300-line function with a cyclomatic complexity of 40; a human reviewer will not catch it at 2am10across 40 repos. So move the ceiling into the linter, run it in CI, and let the agent self-correct11before the PR ever reaches you.1213## The budgets (oxlint)1415Use **oxlint** (`oxc`, https://oxc.rs) — one fast binary, no plugin install, runs in CI and16pre-commit. Default agent budgets:1718| Rule | Cap | Why |19|---|---|---|20| `complexity` | 10 | one function, one job — split, don't raise the cap |21| `max-lines` | 300 | a file over 300 lines is two files |22| `max-lines-per-function` | 50 | a function over 50 lines hides its own bugs |23| `max-params` | 4 | more than 4 params → pass an object |24| `max-depth` | 4 | deep nesting is un-reviewable |25| `max-nested-callbacks` | 4 | same |26| coverage | ratchet | every PR must not lower codecov; new code carries tests |2728The canonical `.oxlintrc.json` + workflow live in `~/oxlint-rollout/` on the author's machine.2930## Ship it warn-first, then ratchet — the only way to fan out3132Strict-as-**error** is a trap for an existing fleet. Measured on one real repo (offrouter, 702 TS33files): strict budgets produced **~470 blocking errors** — 266 complexity, 121 long-function, 4234big-file, plus hundreds of `correctness`/`perf` category errors. Multiply across 40 repos and you35have a multi-week refactor campaign, not a rollout — and every PR red-CIs and blocks its own merge.3637So: **every rule ships as `warn`.** CI stays green, the PR auto-merges, and the violation counts38become visible on every run. Then ratchet per repo over time — promote `correctness` to error39first (real bugs), then the size/complexity budgets as you burn them down. A simple counter beats40a grand plan: pick one repo, fix its top violations, flip those rules to error, move on.4142## Three gotchas that will bite you (all learned the hard way)43441. **oxlint has NO `no-restricted-syntax`.** The React "deslop" hook bans (below) CANNOT ride on45 oxlint — the config fails to parse with `Rule 'no-restricted-syntax' not found`. Budgets live in46 oxlint; the React bans live in **eslint**. Two linters, two jobs.472. **Parse errors can't be downgraded by config.** oxlint keeps a couple of parser-level errors48 (`Empty parenthesized expression`, missing-semicolon on some TSX) at error severity no matter49 what `categories` say — so the job exits 1 even in an all-`warn` config. Fix: the CI step runs50 `bunx oxlint@^1 --config .oxlintrc.json || true` during the warn-first phase. Drop the `|| true`51 when you ratchet that repo to enforcement.523. **Most personal repos have "Allow auto-merge" OFF.** `gh pr merge --auto` then fails. Either the53 PR is immediately mergeable (direct `--squash`) or it is BLOCKED by a required review — do NOT54 admin-bypass a required review to land a lint config; that defeats the guardrail. Leave it for55 the one-click approval.5657## React feature code — the deslop ban-list (eslint, warn-first)5859In **feature** code, derive state; do not store it. Ban via eslint `no-restricted-syntax`60(CallExpression selectors) at `warn`, then flip to `error`:6162- No `useState` / `useEffect` / `useLayoutEffect` / `useReducer` / `useSyncExternalStore` — derive63 from the backend + URL params; load data through a loader-initiated `useQuery` (tanstack router64 `preload="instant"` makes the route instant).65- No `useMemo` / `useCallback` — the React Compiler handles memoization.66- No overzealous destructuring; no type casting outside tests.6768These are style, not correctness — an `eslint-disable` with a one-line reason is the rare escape69hatch, never the default fix.7071## Rollout mechanics (fleet-scale)7273- **Fan-out unit = 2 self-contained files** (`.oxlintrc.json` + a standalone `oxlint.yml` that runs74 `bunx oxlint` on `ubicloud-standard-4` — private `pooriaarab/*` repos run every job on an Ubicloud75 runner, and the self-hosted Dell fleet is retired), independent of each repo's existing76 turbo/eslint pipeline. Uniform, minimal blast radius, no per-repo bespoke wiring.77- **No local clone needed** — create the branch and both files through the `gh` contents API78 (the token needs `workflow` scope; a git *push* of a workflow file works, but the contents-API79 edit of a workflow is classifier-gated, so patch workflows via clone+push).80- **Skip forks and empty scaffolds.** `isFork==true` → never lint someone else's code. A 3–4 KB81 repo is an empty scaffold — skip it.82- **Deploy guard before merge.** Check whether the PR's base branch has a deploy/release workflow83 triggered by push to it. On the standard layout the default branch `main` deploys to the staging84 environment and only `release` deploys production, so a `main` merge is safe and a PR based on85 `release` is not. Merge freely where the base deploys to staging or a demo; hold where merging it86 deploys production. `staging` and `production` are environment names — never retarget to a branch87 by those names, and never create one.88- **Check the trigger against the branches that exist.** A `branches:` filter naming a branch the89 repo does not have never fires and never errors, so a fanned-out workflow can land in dozens of90 repos and run in none of them. Read `git branch -r` (or the repo's default branch from the API)91 before you trust a green fan-out.9293## Beyond lint — the fuller agentic guardrail stack9495Lint budgets are one layer. The complete stack for high-quality agent-authored code:96971. **Lint budgets** (this skill) — complexity, size, deslop.982. **Coverage ratchet** — a CI gate that fails when a PR lowers coverage; new code carries tests.993. **Asset/bundle byte budget** — a size check on built JS/CSS so an agent can't 3× the bundle.1004. **Typecheck as a required check** — `tsc --noEmit`, no `any` (oxlint `no-explicit-any`).1015. **Secret + dependency scanning** — gitleaks on every PR; Dependabot/`bun update` cadence.1026. **A review gate** — an LLM council or `claude-review` that must approve, plus one human click on103 protected branches. Do not let agents bypass it.1047. **CI economics** — at agent PR volume, add concurrency-cancel + path filters (see the105 `ci-cost-at-agent-scale` skill) before the bill surprises you.106107Roll each out the same way: warn/advisory first across the fleet, then ratchet to blocking per repo.108109## Related110111- [pr-standards](../pr-standards/SKILL.md) — the pull-request rule these budgets keep a PR inside: one issue, one concern, under 500 lines, with proof of the work.