DEV Community

Ahsan Ali
Ahsan Ali

Posted on

CLAUDE.md is not a quality gate. Here's how to build one with hooks

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.

Top comments (1)

Collapse
 
skillselion profile image
Skillselion •

One edge on the Stop gate: a suite that cannot go green (a database that is down, a missing secret) keeps sending Claude back. The hooks reference says the Stop input carries stop_hook_active, true when Claude is already continuing because of a stop hook, and that Claude Code overrides the block after eight consecutive continuations. It adds that the count resets each time Claude calls a tool, so as we read it, an agent that edits between failures can stay under the cap. The one-line wrapper in the post has no way to read that flag, so the cap is its only exit. Would you move the check into a script that reads stdin? Also, the same page lists exit 2 on PostToolUse as non-blocking, because the edit already ran.