DEV Community

Cover image for 3 Claude Code patterns: read-only subagents, Haiku routing, and hooks that block with exit code 2
The Agent Runbook
The Agent Runbook

Posted on

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 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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

.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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (4)

Collapse
 
skillselion profile image
Skillselion •

There is one fail-open path that "onFailure": "block" won't catch, because the script never fails. If jq is missing, cmd=$(jq ...) just sets cmd to an empty string, grep finds nothing, and guard.sh reaches exit 0. We ran it with jq off the PATH and it returned 0 on rm -rf /tmp/x. A first line fixes it:

command -v jq >/dev/null || { echo "guard: jq missing" >&2; exit 2; }

The same run also let through rm -r -f, rm --recursive --force and git push origin +main (a force push by refspec), which backs your speed-bump point, and the deny rule needs the same variants, since Bash(rm -rf *) misses them too.

Collapse
 
sherotree profile image
Sherotree •

The tool allowlist vs system-prompt distinction is the part that stuck. I've had "do not edit" reviewers quietly rewrite files more than once — locking tools to Read/Grep/Glob is the only version that actually held.

Collapse
 
paul_irolla_6b5ae261d0224 profile image
Paul Irolla •

Working on Tokenade, I would keep the allowlist and the blocking hook over the instruction. A system prompt asks the model to stay inside a tool. An allowlist means a tool outside it does not run at all.

Collapse
 
ranthis profile image
RanThis •

matches what we measured: haiku 5.5 and sonnet 5.5 both scored 15/15 on three graded tasks, haiku at $0.0013 vs $0.024. those were extraction-style tasks though, so "promote it when it misses things" is the right hedge for anything needing judgment