Types, linters and static checks as the first filter

Automate the mistakes an agent makes most often, and wire the checks into the loop so the agent fixes them before you ever read the diff.

Types catch the mechanical errors

// strict mode turns these into compile errors, not runtime surprises
type Order = { id: string; total: number; currency: "GBP" | "USD" };

// 1. Property access on a maybe-missing value
function label(o: Order, note?: string) { return note.toUpperCase(); }
// 'note' is possibly 'undefined'.

// 2. A literal that is almost right
const c: Order["currency"] = "GBX";
// Type '"GBX"' is not assignable to type '"GBP" | "USD"'.

// 3. An unchecked index
const first = orders[0].id;    // with noUncheckedIndexedAccess: possibly undefined

// 4. A forgotten case in a switch
function fee(k: Kind): number {
  switch (k) { case "card": return 1; }
  // Function lacks ending return statement and return type is not 'undefined'.
}
npx tsc --noEmit          # the fastest signal available
npx tsc --noEmit --pretty false   # compact output, easier to paste into a prompt

Hand the compiler output to the agent as the task. It is unambiguous, it names the location and it cannot be argued with - which makes it a far better correction channel than your opinion about the code.

Rules that catch agent habits

{
  "rules": {
    "no-console": "error",
    "no-empty": ["error", { "allowEmptyCatch": false }],
    "no-floating-promises": "error",
    "no-explicit-any": "error",
    "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
    "eqeqeq": ["error", "always"],
    "no-return-await": "error",
    "require-await": "error",
    "no-throw-literal": "error"
  }
}
  • no-explicit-any blocks the escape hatch generated code reaches for when types get hard.
  • no-empty with empty catches disallowed stops the silent-swallow fix.
  • no-floating-promises catches the async call nobody awaited, which is a common generated bug.
  • require-await and no-return-await remove async noise that hides real behaviour.
  • Run the linter with --max-warnings 0 so warnings behave like errors.
⚠️
Do not add a rule the day you need the agent to fix it. Tighten the configuration on an existing clean codebase, commit that separately, and only then let generated code meet the new rules. A large pre-existing violation set turns every check into noise the agent learns to ignore.

Wiring the checks into the loop

{
  "scripts": {
    "check": "npm run typecheck && npm run lint && npm test -- --run",
    "typecheck": "tsc --noEmit",
    "lint": "eslint . --max-warnings 0"
  }
}
"Before you report a task complete, run npm run check and show me
the output. If it fails, fix it and run it again."
CheckCatchesSpeed
Type checkerWrong names, shapes, nullabilitySeconds
LinterSwallowed errors, unused code, bad asyncSeconds
FormatterDiff noise, review confusionInstant
Unit testsWrong behaviour on known casesSeconds to minutes
End-to-end testsBroken integration, missing wiringMinutes
# pre-commit, so a bad state cannot be committed at all
npx husky add .husky/pre-commit "npm run check"

# and in CI, identically
- run: npm run check

Running the same command locally, in a hook and in CI means there is exactly one definition of acceptable, and the agent can find it without being told twice.

FAQ

Do I need a linter if I have strict types?
Yes. Types describe data shapes; they do not catch an empty catch block, an unawaited promise, or a leftover console call. The two overlap on almost nothing, and both are cheap to run.
What about formatting?
Use a formatter on save and never discuss it in a prompt. Formatting consistency is what makes an agent's diff readable; without it, a real three-line change arrives wrapped in two hundred lines of reindentation.

Specs and tests as the contract Choosing a stack that agents handle well

Last refreshed 2026-09-18.