Most devs think writing "always run tests before finishing" in CLAUDE.md is enough. It isn't.
CLAUDE.md is context, not enforcement. The agent reads it, and most of the time it follows it. Most of the time isn't a quality gate.
Long session, big context, a tricky fix, and suddenly it says "Done ✅" while your build is red.
If a check matters, don't ask for it. Enforce it.

Hooks

Hooks are shell commands Claude Code runs automatically at set points. The agent can't skip them, forget them or talk its way past them. Here's a real quality gate in .claude/settings.json:

1. PostToolUse: fast checks after every edit

Matcher: Edit|Write. Run eslint --fix on just the file that changed. The file path comes in on stdin, so it's scoped and quick.

2. Stop: the hard gate before the agent can finish

Run the full typecheck and tests: tsc --noEmit && vitest run. If they fail, the agent can't end its turn. It gets the errors and has to fix them.

3. The part most people miss: exit code 2

Only exit code 2 blocks the agent and sends the error back to it. tsc and vitest exit with 1 when they fail, so the agent just carries on. Wrap it:
(tsc --noEmit && vitest run) >&2 || exit 2

The loop closes by itself

Agent writes code → check fails → agent reads the error → fixes it → tries again. You stop being the agent's test runner.
CLAUDE.md tells the agent what you want. Hooks make sure it actually happens.