Dev.to AI ๐Ÿค– Ai ๐Ÿ‘ 0 ๐Ÿ“– 5 min read

3 Claude Code patterns: read-only subagents, Haiku routing, and hooks that block with exit code 2

Most Claude Code setups stop at a CLAUDE.md file. The bigger levers sit one layer down: subagents with locked-down tools, per-agent model choice, and hooks that enforce rules instead of asking nicely. Here are three patt

Most Claude Code setups stop at a CLAUDE.md file. The bigger levers sit one layer down: subagents with locked-down tools, per-agent model choice, and hooks that enforce rules instead of asking nicely. Here are three patterns you can paste into a repo today, each with the gotcha that usually bites.

Everything below was checked against the official Claude Code docs (code.claude.com/docs) in October 2026. The CLI changes quickly, so if a field has moved, claude --help and the docs win.

Pattern 1: a reviewer subagent that physically cannot edit files

A code reviewer that can edit will "helpfully" fix the thing you wanted flagged. A system prompt saying "do not edit" is a request. A tool allowlist is a constraint.

Create .claude/agents/reviewer.md:

---
name: reviewer
description: "Read-only code reviewer. Use proactively after code changes to flag bugs, security issues, and convention drift."
tools: Read, Grep, Glob
model: sonnet
---

You are a senior reviewer. Report findings by severity with file:line
citations. You have no write tools. Return a findings list for the main
agent to act on; do not try to fix anything yourself.

The gotcha: per the docs, the tools field is an allowlist, and a subagent "inherits every tool available to subagents if omitted." That includes Edit and Write, and Bash. The classic mistake is a carefully worded read-only prompt with no tools line.

Two more things worth knowing:

  • If you would rather deny than allow, disallowedTools removes tools from the inherited set. To get a truly read-only agent that way, list Write, Edit, NotebookEdit, Bash. Deny only Write and Edit and the inherited Bash can still write files with a shell redirect. The allowlist above is the simpler route.
  • Edit the file in .claude/agents/ directly. The /agents command no longer opens a management wizard in current versions; it just prints a reminder to do that.
  • Frontmatter field names must match exactly (maxTurns, disallowedTools). Claude Code ignores a field it does not recognize without reporting an error, so a typo means a silently missing guardrail.

Do not give it Bash and still call it read-only. A shell can write files with a redirect. If the reviewer needs to run commands, give it Bash and add deny rules for the destructive patterns.

Test it once: ask the reviewer to create a file. It should tell you it cannot.

Pattern 2: put each subagent on the cheapest model that can do its job

The model field on a subagent takes an alias (sonnet, opus, haiku), a full model ID, or inherit. If you omit it, the subagent follows the main conversation's model by default.

A scout that only finds files and call sites does not need your strongest model:

---
name: scout
description: Fast codebase scout. Finds files and call sites, returns a map. Use before any non-trivial change to locate the relevant code.
tools: Read, Grep, Glob
model: haiku
---

Return only: matching files, the symbols involved, and a one-line note
for each. No analysis, no recommendations.

Two benefits, and the second is often the bigger one:

  1. Retrieval work runs on a cheaper model tier.
  2. The subagent works in its own context window. Thousands of lines of search output never enter your main conversation; you get the summary, not the noise.

Keep synthesis and design decisions on the stronger model in the main thread. A practical rule: start retrieval agents on haiku, and promote one only when you see it missing things. Run /usage to see what a session actually cost instead of guessing.

Pattern 3: a PreToolUse hook that blocks a command and explains why

Hooks are deterministic. The docs are direct about the difference: CLAUDE.md is context, not enforced configuration, and to block an action regardless of what Claude decides you use a PreToolUse hook.

A PreToolUse hook that exits with code 2 blocks the tool call, and Claude sees your stderr message as the reason. So the model can correct course instead of just failing.

.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard.sh"
          }
        ]
      }
    ]
  }
}

.claude/hooks/guard.sh (run chmod +x on it, and install jq):

#!/usr/bin/env bash
# The hook receives the tool call as JSON on stdin.
cmd=$(jq -r '.tool_input.command // ""')

if printf '%s' "$cmd" | grep -qE '(^|[^[:alnum:]_])rm +(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)|git +push +(.* )?(--force|-f)( |$)'; then
  echo "Blocked: '$cmd' is destructive. Use a safer alternative (trash, --force-with-lease) and say why you need it." >&2
  exit 2
fi
exit 0

This blocks rm -rf and rm -fr, and blocks git push --force and -f while still allowing --force-with-lease. Run /hooks inside Claude Code to confirm it loaded.

The gotchas:

  • Exit codes matter. Exit 0 lets the call continue through the normal permission flow. Exit 2 blocks. Any other non-zero code, including a crash that exits 1, is a non-blocking error and the action goes ahead. A guard that fails open is worse than no guard. On Claude Code v2.1.295 and later, add "onFailure": "block" next to "type" in the hook entry so a crash, timeout or missing script blocks the call like exit 2 does. Either way, test yours by piping fake input in by hand: echo '{"tool_input":{"command":"rm -rf /tmp/x"}}' | .claude/hooks/guard.sh; echo $? should print the message and 2.
  • A regex is a speed bump, not a sandbox. A determined or confused agent can phrase a destructive command a way your pattern misses. For hard denies, pair the hook with a permission deny rule such as "permissions": { "deny": ["Bash(rm -rf *)"] }.
  • Exit 2 is not the biggest hammer. If a hook prints JSON with "continue": false, Claude stops processing entirely. Use exit 2 for "do it differently" and keep continue: false for "stop everything."

Putting the three together

The reviewer cannot edit, the scout is cheap and keeps your context clean, and the guard hook holds even when a prompt is ignored. None of this needs a framework: three small files in .claude/.

More from The Agent Runbook. These three patterns are free and complete as written. I also sell material that goes further. This is my own work, so weigh it accordingly.

One new play a week: https://theagentrunbook.substack.com

The Agent Runbook is an independent publication. It is not affiliated with or endorsed by Anthropic.

๐Ÿ“ฐ Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes โ€” full credit and traffic to the original publisher.