Hooks and Custom Subagents

From Autocomplete to Coworker

hookssubagentsPostToolUseparallel dispatchpolicy

I once typed “please run prettier on this file” 47 times in one week. Then I learned about hooks. I haven’t typed it since.

That sentence is the entire pitch for this chapter. If you’ve ever found yourself nagging to do the same five things every turn — format, lint, run the test, write a sane commit message, ping you when it finishes — you don’t have a prompt problem. You have a policy problem. And the answer is two features most people skim past on day one: hooks and subagents.

Hooks turn ad-hoc prompting into policy. Subagents turn one model into a team. Together, they’re how you stop talking to Claude and start operating it.


What hooks actually are#

A is a shell command (or HTTP endpoint, or short LLM prompt) that Claude Code runs automatically at specific points in its lifecycle. They live in your settings file, not in the chat. The model can’t forget them, can’t skip them, and can’t be sweet-talked out of them by a clever prompt injection.

There are a lot of events, but in practice you’ll spend 90% of your time on three:

The full list also includes SessionStart, UserPromptSubmit, PostToolUseFailure, PermissionRequest, TaskCompleted, WorktreeCreate, and a handful of others. Don’t enumerate them in your head — look them up when you need them. The three above pay for themselves in week one.


Where hooks live#

Two locations:

Here’s the shape of a real PostToolUse hook that runs Prettier after every edit:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "prettier --write \"$CLAUDE_FILE_PATH\"" }
        ]
      }
    ]
  }
}

The matcher field is a regex against the tool name — here, “Edit or Write.” $CLAUDE_FILE_PATH is one of several environment variables Claude Code injects when the hook runs. There are others: $CLAUDE_PROJECT_DIR, the tool name, the full JSON payload on stdin if you want it.

That’s the whole concept. Now we make it useful.


Five hooks every team should run#

1. format-on-save#

{
  "matcher": "Edit|Write",
  "hooks": [
    { "type": "command", "command": "prettier --write \"$CLAUDE_FILE_PATH\" 2>/dev/null || ruff format \"$CLAUDE_FILE_PATH\"" }
  ]
}

Stops “please format this” forever. Picks Prettier first, falls back to Ruff. Failure is silent so non-JS/Python files don’t blow up the chain.

2. test-on-write#

{
  "matcher": "Edit|Write",
  "hooks": [
    { "type": "command", "command": "if [[ \"$CLAUDE_FILE_PATH\" == *.test.* ]]; then npx vitest run \"$CLAUDE_FILE_PATH\"; fi" }
  ]
}

Wrote to a test file? Run that one test. Tight feedback loop, no orchestration logic in the chat.

3. block-push-to-main#

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        { "type": "command", "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -q 'git push origin main'; then echo 'Blocked: push to main requires a human.' >&2; exit 1; fi" }
      ]
    }
  ]
}

Non-zero exit blocks the tool call. The stderr message goes back into the model’s context, so the agent reads “Blocked: push to main requires a human.” and adapts. This single hook has saved me from three “oh no” moments at Belkins this year.

4. commit-message-template#

{
  "Stop": [
    {
      "hooks": [
        { "type": "command", "command": ".claude/hooks/draft-commit.sh" }
      ]
    }
  ]
}

Where draft-commit.sh runs git diff --cached, summarizes it (with a tiny local model or a templated heuristic), and writes the message to .git/COMMIT_EDITMSG. The agent ends its turn, you get a pre-filled commit message, you tweak and ship.

5. slack-notify-on-long-task#

{
  "Stop": [
    {
      "hooks": [
        { "type": "command", "command": "[ \"$CLAUDE_TURN_DURATION_MS\" -gt 120000 ] && curl -X POST -d \"text=CC turn finished: $CLAUDE_SESSION_ID\" $SLACK_WEBHOOK" }
      ]
    }
  ]
}

Two-minute threshold, Slack DM, done. Now I can launch a long refactor, walk away, and trust the buzz on my watch instead of polling the terminal like a madman.


Hooks return values matter#

This is the part most people miss. Hooks aren’t fire-and-forget. They have a contract:

Which means a hook is also a way to talk back to Claude. Two patterns I lean on:

Hooks aren’t just guardrails. They’re a side-channel for policy.


Subagents — the 90-second mental model#

A is a child instance you spawn from your main session. It has:

Claude Code ships with built-ins. The names you’ll see:

screenshot
Three-subagent fan-out
Main CC session dispatching general-purpose, Explore, and Plan in a single Agent batch, with three result summaries returning in parallel.
id: 16-hooks-subagents-1 · drop 16-hooks-subagents-1.png into public/screens/

Custom subagents — writing your own#

Subagents are Markdown files with YAML frontmatter. Two locations, same rules as hooks:

Here’s a code-reviewer subagent I use across Belkins and Folderly:

---
name: code-reviewer
description: Reviews diffs for security, performance, style. Use when user says
  "review this PR", "check this diff", or "is this code safe?". Read-only.
tools: Read, Grep, Glob
---

You are a senior code reviewer. Given a diff, return:
1. Three highest-impact issues, ranked by severity.
2. Two style nits, with line numbers.
3. One suggestion the original author would not have considered.

Do not edit. Do not run shell. Read-only review only.

Three things to notice. The description is what the orchestrator reads to decide when to delegate — write it like an instruction, not like a tagline. The tools line is a tool allow-list — by stripping Edit, Write, and Bash, I’ve made it physically impossible for this subagent to mutate the repo. And the body is just the system prompt: short, opinionated, formatted output enforced.


Spawning subagents — the practical move#

In your main session, you say (or the model decides on its own):

Spawn 3 code-reviewer subagents in parallel: one on src/auth, one on src/billing, one on src/api. Then merge their findings into a single ranked list.

The orchestrator dispatches all three in one tool batch. Each runs in its own context, reads its slice, and returns a summary. The orchestrator merges them. You get the union of three focused reviews in roughly the time of one — and your main context isn’t polluted with three full diffs.

This is the move. Three reviewers in parallel beats one reviewer reading 9,000 lines, every time.


The four parallel-dispatch patterns#

Chapter 6 covered the why of parallel dispatch — context isolation, throughput, blast radius. This chapter is the how to wire them. Quick re-cap of the four shapes:

If you can name the shape before you dispatch, you’ll write better orchestrator prompts.


Subagent anti-patterns#

Spawning a subagent for a task that fits in one tool call. Subagent dispatch has overhead — context setup, prompt parsing, summary generation. If the task is “read this one file and tell me the export,” just read the file. Subagents pay off when the work is non-trivial or when isolation matters.

Forgetting to specify the output format. If you don’t tell each subagent exactly what shape to return — three bullets, JSON, ranked list — you’ll get N different shapes and your merge step becomes a parsing nightmare. Pin the format in the subagent body or in the dispatch prompt. Both is fine.

Letting subagents share state via the orchestrator’s context. If subagent A produces something subagent B needs, write it to disk and pass the path. Don’t try to thread it through orchestrator memory. Disk handoffs are debuggable, replayable, and don’t melt your context window. Use /tmp or a .claude/scratch/ directory and treat it like a message queue.


Hooks + subagents = real systems#

Now wire them together. Concrete example, from my own setup, for a release flow on the Newsletter publishing repo:

That’s a real system. The agent does the boring work. The hooks enforce the rules. The subagents keep the context clean. And the human (me) only shows up at the one moment where judgment matters: the push to main.

screenshot
Hooks plus subagents — a real release flow
A `.claude/settings.json` open on the left showing the three hooks, and a `.claude/agents/` directory listing on the right with `code-reviewer.md`, `changelog-writer.md`, `version-bumper.md`, and `smoke-test-runner.md`.
id: 16-hooks-subagents-2 · drop 16-hooks-subagents-2.png into public/screens/

Spotted something wrong, missing, or sharper? Email Vlad with feedback on this chapter →
Stay close

Edition 3 lands when this list says it does.

No course. No paywall. Operator playbooks weekly. 10K+ subscribers.