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 promptHand 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-anyblocks the escape hatch generated code reaches for when types get hard.no-emptywith empty catches disallowed stops the silent-swallow fix.no-floating-promisescatches the async call nobody awaited, which is a common generated bug.require-awaitandno-return-awaitremove async noise that hides real behaviour.- Run the linter with
--max-warnings 0so 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."| Check | Catches | Speed |
|---|---|---|
| Type checker | Wrong names, shapes, nullability | Seconds |
| Linter | Swallowed errors, unused code, bad async | Seconds |
| Formatter | Diff noise, review confusion | Instant |
| Unit tests | Wrong behaviour on known cases | Seconds to minutes |
| End-to-end tests | Broken integration, missing wiring | Minutes |
# 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 checkRunning 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.
Related
Specs and tests as the contract Choosing a stack that agents handle well
Last refreshed 2026-09-18.