{"slug": "stop-asking-your-ai-agent-to-follow-rules-enforce-them", "title": "Stop asking your AI agent to follow rules. Enforce them.", "summary": "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.", "body_md": "You've written it a hundred times. In your `CLAUDE.md`\n\n, in your system prompt, in ALL CAPS:\n\nNEVER put`\"use client\"`\n\nat the page level. NEVER commit`@ts-ignore`\n\nwithout a reason.\n\nAnd 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.\n\nHere'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.\n\n`exit 2`\n\nturn 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.\n\nBut rereading my playbooks, I noticed the rules split cleanly into two categories.\n\n**Rules that need judgment:**\n\nTrace the Server/Client boundary by hand.\n\nDon't write a fix until you can state the root cause.\n\nThese need a model. Prompts are the right place for them.\n\n**Rules that are just string matching:**\n\n`\"use client\"`\n\nat the top of`app/**/page.tsx`\n\n→ wrong.\n\n`process.env.SECRET`\n\nin a client file → wrong.\n\n`@ts-ignore`\n\nwith no justification → wrong.\n\nWhy 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.\n\nClaude 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`\n\nor `Write`\n\n, and it receives a JSON payload on stdin telling you which file was touched.\n\nThe magic is in the exit-code contract:\n\nNo 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.\n\nA minimal version is embarrassingly simple:\n\n``` bash\n#!/usr/bin/env node\n// .claude/hooks/check.mjs — wired to PostToolUse in .claude/settings.json\nconst input = JSON.parse(await readStdin());\nconst file = input.tool_input?.file_path;\nif (!file?.endsWith(\".tsx\")) process.exit(0);\n\nconst src = fs.readFileSync(file, \"utf8\");\nif (/^\\s*['\"]use client['\"]/.test(src) && /\\/app\\/.*page\\.tsx$/.test(file)) {\n  process.stderr.write(\n    \"route-level \\\"use client\\\": this makes the whole page render client-side. \" +\n    \"Push it down to the smallest interactive leaf component.\"\n  );\n  process.exit(2); // ← this line is the entire trick\n}\n```\n\nThat's a code reviewer that never sleeps, never gets context-drunk, and works for free.\n\nIn [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:\n\n| Team | Checks (excerpt) |\n|---|---|\n`next-ts` |\nroute-level `\"use client\"` , non-`NEXT_PUBLIC_` env in client files, `useEffect` +`fetch` for initial data, `fetch()` without explicit cache intent, `@ts-ignore` /`as any`\n|\n`go-api` |\n`http.Error` not followed by `return` , wrapping errors with `%v` instead of `%w` , errors discarded with `_` , `context.Background()` mid-request |\n`python-fastapi` |\nbare `except:` , Pydantic v1 API, mutable default args, `time.sleep` /`requests.*` inside async code |\n`rails` |\nSQL interpolation in `where` , `update_column` /`save(validate: false)` , `default_scope` , params mass-assignment, `Time.now`\n|\n`django` |\nnaive `datetime.now()` , `fields = '__all__'` , injection-prone `.raw()` /`.extra()` , `post_save` signals |\n`react-native` |\n`.map` inside `ScrollView` , index as key, DOM APIs like `localStorage` , unconditional `behavior=\"padding\"`\n|\n`frontend` |\n`onClick` on a `<div>` , `<img>` without `alt` , `outline: none` with no `:focus-visible` , z-index escalation |\n\nWhen an agent writes a violation, it gets this back instantly:\n\n```\nccteams next-ts check — app/dashboard/page.tsx:\n  - route-level \"use client\": this page and its entire import tree now render\n    client-side. Push \"use client\" down to the smallest interactive leaf component.\n  - client file reads process.env.API_SECRET: non-NEXT_PUBLIC_ env vars are\n    undefined in the browser (or a secret leak if inlined).\nFix these now, or state in your report why each is intentional.\n```\n\nNote the escape hatch in the last line. These are nudges, not walls — the edit already happened, and sometimes `useEffect`\n\n+ `fetch`\n\nis legitimate. The agent can push back with a reason, and the reviewer checks that it did one or the other.\n\n**1. It fires at 100%.** A prompt rule needs to be read, retained, and recalled at the right moment. A hook is a grep. Token 500k? Fires. Haiku wrote the code? Fires.\n\n**2. You pay per violation, not per instruction.** Prompt rules cost tokens on every delegation *even when they're followed*. A hook costs zero tokens until something is actually wrong — then it costs three lines.\n\n**3. Mistakes die before the review round-trip.** Builder writes bug → reviewer catches it → sends it back → builder fixes it: that loop is the expensive part of multi-agent setups. Hooks kill the mistake at write time, so your reviewer spends its (expensive) tokens on things that actually need judgment.\n\nThis is the part I didn't expect to matter so much.\n\nccteams agents ship in two tiers — builders on Sonnet, reviewers on Opus. v0.3.0 adds model profiles:\n\n```\nccteams use next-ts --profile budget    # builder: haiku / reviewer: sonnet\nccteams use next-ts                     # builder: sonnet / reviewer: opus\nccteams use next-ts --profile max       # everyone: opus\n```\n\nGetting Haiku to *remember* a playbook through prompts alone is a losing game. But hooks fire on Haiku's code with exactly the same precision as on Opus's. With deterministic checks backing it from below, `--profile budget`\n\nstops being \"cheap and broken\" and becomes a legitimate configuration for routine work.\n\n**The more discipline you mechanize, the less intelligence you need to rent.**\n\nHooks only cover rules that are mechanically checkable without false positives. \"Trace the data flow,\" \"run the actual build and quote its output,\" \"state the root cause before fixing\" — those stay in prompts and reviewer gates, and I have no intention of moving them.\n\nThe rule of thumb I've landed on:\n\nIf a violation can be detected with grep, enforce it with a hook. If it needs judgment, put it in the prompt.\n\nOne design note if you build this yourself: make your hook scripts fail silent (`catch → exit 0`\n\n). A buggy check that crashes loudly will poison every editing session. And namespace your hook entries (ours all live at `.claude/hooks/ccteams-*`\n\n) so that installing/removing them never touches hooks the user wrote themselves.\n\n```\nnpm install -g ccteams\nccteams use next-ts --profile budget   # or go-api, rails, django, python-fastapi...\n# restart Claude Code — hooks and agents load at session start\n```\n\nOne command gets you the agent team, the playbook, the hooks, and the cost profile. Switching teams swaps the hooks cleanly.\n\nIf this saves you a round-trip or two, a star on the [repo](https://github.com/toffyui/ccteams) genuinely helps. And I'd love to hear what checks you'd add for your stack — the whole point of this design is that a new rule is one regex away.", "url": "https://wpnews.pro/news/stop-asking-your-ai-agent-to-follow-rules-enforce-them", "canonical_source": "https://dev.to/toffy/stop-asking-your-ai-agent-to-follow-rules-enforce-them-4mlo", "published_at": "2026-08-25 15:25:37+00:00", "updated_at": "2026-08-25 15:44:58.889717+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-tools"], "entities": ["Claude Code", "ccteams", "Anthropic", "Next.js", "Go", "Python", "FastAPI", "Rails"], "alternates": {"html": "https://wpnews.pro/news/stop-asking-your-ai-agent-to-follow-rules-enforce-them", "markdown": "https://wpnews.pro/news/stop-asking-your-ai-agent-to-follow-rules-enforce-them.md", "text": "https://wpnews.pro/news/stop-asking-your-ai-agent-to-follow-rules-enforce-them.txt", "jsonld": "https://wpnews.pro/news/stop-asking-your-ai-agent-to-follow-rules-enforce-them.jsonld"}}