When I started using Claude, my quality gates were already in place. Zero warnings, TypeScript strict mode, every ESLint rule as error, lint-staged on every commit. I've been using that workflow years before AI was a factor.

Not "a few warnings are fine." Not "we'll clean those up later." Zero.

I've seen what Claude generates raw, before the linter forces it to refactor. It's the same patterns I saw engineers complaining about online. I knew the gates were going to be the answer. They've done exactly that.

Your codebase teaches the AI

AI agents are pattern-matching systems. They learn from what's in your codebase. A messy, inconsistent codebase with silenced warnings and undefined behavior produces AI output that's messy, inconsistent, and quietly wrong. The model isn't doing something unexpected. It's mirroring your codebase.

Zero warnings means Claude has a clean signal. Every commit that passes the gates is a concrete example of what "correct" looks like for this project. When Claude generates code that fails the linter, it reads the error, fixes it, and commits again. It self-corrects on every change, with no intervention from me.

A quality gate isn't a guardrail you bolt on after the fact. It's how you teach AI what "good" means for your specific codebase from the start.

Encode the standard, don't prompt it

Here's a pattern I hit early. Claude wrapped every event handler in useCallback, not occasionally but every single one. That's one of the most common anti-patterns in public React code, and Claude had clearly absorbed a lot of it.

I could have written a long prompt explaining when useCallback is appropriate. I did the better thing: I fed Claude React's official hook documentation, we established the criteria for when memoization is actually warranted, and I folded those criteria into a React skill that loads on every React file. From that point on, the standard was in front of Claude automatically, every session.

The change was immediate. Hooks were implemented correctly in every component and every file, and my trust in the output went up.

The pattern generalizes: when Claude gets something consistently wrong, the fix isn't a better prompt. It's encoding the standard into something Claude reads automatically, every session, without being asked.

Make it structurally impossible

ESLint handles code quality. Hooks handle behavior. These are different in kind.

Claude Code has a hooks system: shell scripts that run before or after specific actions. Two I use constantly:

First, I block Claude from editing the ESLint configuration. Sometimes when Claude hits a linting error it can't resolve in a few attempts, it tries to change the rule itself. That's exactly backwards. The rule exists because I put it there deliberately. This hook intercepts any attempted edit to the ESLint config and rejects it, forcing Claude to fix the code instead.

#!/bin/bash
# Exit 2 = block the tool call. stderr is shown to Claude as the reason.
 
file_path=$(jq -r '.tool_input.file_path // ""' < /dev/stdin)
 
if echo "$file_path" | grep -Eq '(^|/)eslint\.config\.(js|cjs|mjs|ts)$'; then
  echo "BLOCKED: Do not modify eslint.config.* to fix lint errors. Fix the ESLint error in the source file where it occurs." >&2
  exit 2
fi
 
exit 0

Second, I block git commit and force-push on the main branch. Without this, Claude will occasionally commit and push in-progress work to main when I've forgotten to create a branch first. The hook catches the attempt and rejects it, telling Claude to create a feature branch first. Claude makes the branch, and the work lands there instead.

Claude wrote both hooks. The ESLint one took under a minute. The git hook took a few iterations as edge cases turned up, but I never had to write the bash myself.

When Claude keeps trying to do things you don't want, make them structurally impossible. A hook that blocks an action is deterministic in a way that a prompt asking nicely is not.

What it costs

On a greenfield project, zero warnings costs almost nothing. You set the bar before any code exists. On a legacy codebase it can be a time-consuming conversion, full of eslint-disabled comment cleanups and real bugs surfacing in code you thought was working. The cost is real, but it's only paid once. After that, every commit is on the right side of the line.

That's why so many teams have lived without strict linting on brownfield projects. The cleanup costs too much to justify by hand. AI changes that. It can do the cleanup across an entire codebase in a fraction of the time.

Discipline before AI

When I started using this workflow years ago, I thought I was just being a disciplined engineer. It turns out it was the best preparation for agentic development I could have done.

If you're building with AI on a codebase without quality gates, you're managing the model's behavior through prompts alone. That's tiring and unreliable. The more of your standards you enforce deterministically, rather than through conversation, the more you can trust the output, and the less time you spend correcting it.

Quality gates give you leverage. They scale. A rule you write once enforces itself on every change, every session, without anyone having to remember it.

Prompts can be ignored. Deterministic rules cannot.