Stop asking your AI agent to follow rules. Enforce them. A developer behind the open-source ccteams project has introduced deterministic code-review hooks for Claude Code that enforce coding rules via regex-based checks instead of relying on the AI agent's memory. The PostToolUse hook runs after every file edit and exits with code 2 to force the agent to fix violations in the same turn, covering patterns like route-level 'use client' in Next.js and error-handling mistakes in Go. The approach splits rules into judgment-based prompts and string-matching checks, with the latter now automated for stack-specific teams. You've written it a hundred times. In your CLAUDE.md , in your system prompt, in ALL CAPS: NEVER put "use client" at the page level. NEVER commit @ts-ignore without a reason. And your agent does it anyway. Not always — that would almost be easier to deal with. It follows the rule for the first 50k tokens, then quietly stops. Or Sonnet follows it and Haiku doesn't. Or it follows nine rules and forgets the tenth. Here's the thing I finally accepted: a rule in a prompt is a request. The model can decline it. So I stopped asking, and started enforcing. exit 2 turn those rules into a deterministic reviewer that runs after Some background in three lines: I run Claude Code with orchestrated agent teams — a builder writes code, a reviewer verifies it, and both get a stack-specific "playbook" of rules distilled from the mistakes mid-tier models actually make. It works well. I wrote about the prompt-engineering side of it before. But rereading my playbooks, I noticed the rules split cleanly into two categories. Rules that need judgment: Trace the Server/Client boundary by hand. Don't write a fix until you can state the root cause. These need a model. Prompts are the right place for them. Rules that are just string matching: "use client" at the top of app/ /page.tsx → wrong. process.env.SECRET in a client file → wrong. @ts-ignore with no justification → wrong. Why was I asking a language model to remember these? A regex doesn't get tired at 200k tokens. A regex doesn't perform worse on a smaller model. I was running deterministic checks on the most expensive, least reliable runtime available. Claude Code has hooks https://docs.anthropic.com/en/docs/claude-code/hooks — commands that run on lifecycle events. The one you want is PostToolUse : it fires every time the agent runs Edit or Write , and it receives a JSON payload on stdin telling you which file was touched. The magic is in the exit-code contract: No human in the loop. No approval dialog. The agent edits a file, and — from its point of view — the edit "responds" with a code review. It reads the feedback and fixes the problem in the same turn , before a human or a reviewer agent ever sees the mistake. A minimal version is embarrassingly simple: bash /usr/bin/env node // .claude/hooks/check.mjs — wired to PostToolUse in .claude/settings.json const input = JSON.parse await readStdin ; const file = input.tool input?.file path; if file?.endsWith ".tsx" process.exit 0 ; const src = fs.readFileSync file, "utf8" ; if /^\s '" use client '" /.test src && /\/app\/. page\.tsx$/.test file { process.stderr.write "route-level \"use client\": this makes the whole page render client-side. " + "Push it down to the smallest interactive leaf component." ; process.exit 2 ; // ← this line is the entire trick } That's a code reviewer that never sleeps, never gets context-drunk, and works for free. In ccteams https://github.com/toffyui/ccteams v0.3.0, every stack-specific team now bundles a check script built from its playbook's known failure patterns: | Team | Checks excerpt | |---|---| next-ts | route-level "use client" , non- NEXT PUBLIC env in client files, useEffect + fetch for initial data, fetch without explicit cache intent, @ts-ignore / as any | go-api | http.Error not followed by return , wrapping errors with %v instead of %w , errors discarded with , context.Background mid-request | python-fastapi | bare except: , Pydantic v1 API, mutable default args, time.sleep / requests. inside async code | rails | SQL interpolation in where , update column / save validate: false , default scope , params mass-assignment, Time.now | django | naive datetime.now , fields = ' all ' , injection-prone .raw / .extra , post save signals | react-native | .map inside ScrollView , index as key, DOM APIs like localStorage , unconditional behavior="padding" | frontend | onClick on a