Strict Tooling Makes AI Write Better Code
2026-01-28
There's a counterintuitive trick to getting better code from Claude Code: make it impossible for bad code to land. Not "hard" — impossible.
The setup is straightforward. A strict TypeScript configuration with no escape hatches: strict: true, noUncheckedIndexedAccess, noUnusedLocals, noUnusedParameters, exactOptionalPropertyTypes. Biome (or Ultracite, if you want zero-config) handling formatting and linting with no overrides. Husky running pre-commit hooks that block anything that doesn't pass. And the piece that ties it all together: Claude Code tool use hooks that validate every file write against these same rules.
The tool use hook is the critical part. In Claude Code, you can configure hooks that run before or after tool calls. I have a hook that runs the TypeScript compiler and Biome on any file that Claude writes or edits. If the check fails, Claude gets the error output and has to fix it before moving on. It can't just dump code and hope for the best — the feedback loop is immediate and mandatory.
What happens in practice is interesting. The model adapts. When it knows that any will be rejected, it stops reaching for any. When it knows unused imports will fail the commit, it stops leaving them in. When it knows the formatter will rewrite its style choices, it starts writing in the expected style from the first attempt. The constraints don't slow it down — they redirect its output toward correctness.
This is the opposite of how most people use AI coding tools. The common approach is permissive: let the model generate whatever it wants, then review manually. That puts the quality burden on you. The strict approach inverts this: the tooling enforces quality automatically, and your job is to evaluate intent and architecture rather than catching missing semicolons or unsafe type casts.
The practical difference is significant. Without strict tooling, maybe 70% of AI-generated code is correct on the first pass and you spend time fixing the rest. With strict tooling, the model iterates until 100% passes the checks, and you spend your time on whether the approach is right rather than whether the syntax is right.
Husky's pre-commit hook is the last line of defence. Even if something slips past the tool use hook, it gets caught at commit time. The combination of real-time feedback (tool hooks) and gate-based validation (pre-commit) creates a system where bad code genuinely cannot enter the repository. Not "shouldn't" — can't.
A hard-won lesson: lint-staged must run format only, not lint. biome check (which combines format and lint) will catch pre-existing lint warnings in staged files that aren't part of the current change. lint-staged only passes staged files to the command, so if a staged file already had a warning from last month, biome check fails on code you didn't touch. The fix is simple: lint-staged runs biome format --write on staged files, and lint runs separately as a full-project check. Format is safe to scope per-file. Lint needs the full picture.
The same principle applies to the Claude Code hook. Format per-file is instant and safe. Lint per-file catches most issues in real-time. Typecheck needs the full project graph — fast enough for small codebases, but in a monorepo you defer it to the pre-commit hook where it runs once, not on every file write.
Never write manual git stash push --keep-index / git stash pop in pre-commit hooks. lint-staged handles stashing safely with proper rollback. If you write the stash logic yourself and any step fails, conflict markers get baked into the working tree. Every subsequent commit re-introduces them. It's an unrecoverable loop that corrupts your entire working directory. I learned this the hard way across two separate repositories before ripping out the manual stash logic and replacing it with four lines.
One unexpected benefit: the model's code starts looking more consistent with your existing codebase over time within a session. When every deviation gets rejected, the model learns (within the context window) what "correct" looks like for your project specifically. It stops generating generic patterns and starts matching your conventions. Strict tooling is essentially a teacher.
The lesson generalises beyond AI. Any system that tolerates low-quality output will produce low-quality output. The constraints aren't overhead — they're the mechanism that produces quality. Make the boundaries rigid and the work inside them gets better.