Lesson 8 of 9 · Claude Code

Subagents and hooks

A subagent is a second Claude with its own context that does a job and reports back. A hook is a shell command Claude Code runs at a fixed moment whether Claude remembers to or not. One adds hands, the other adds rules.

Everything in this module so far has been about what Claude reads: CLAUDE.md, skills, plugins, MCP servers. This lesson is about two things that change how the work itself runs. Subagents let one session split into workers, each with a clean slate. Hooks let you bolt a shell command onto a moment in the session, so a check runs every time, not most times. As of September 2026 both are built in and both are configured with plain files.

What a subagent is

The Claude Code docs define a subagent as a specialized assistant that handles a specific type of task in its own context window. That phrase, own context window, is the whole point. The main conversation carries everything you have said and everything Claude has read. A subagent starts empty. It gets its own system prompt (the standing instructions that shape its behavior), its own tool list, and optionally its own model. It does the job, then returns only a summary to the main conversation. The search results, the logs, the forty files it opened stay in its context and are thrown away.

Three situations call for one, and the docs name all three. First, parallel work: three workers exploring three parts of a codebase at once. Second, a reviewer that has not seen your reasoning. If you ask the same Claude that wrote the code to review it, it already believes the code is right. A fresh subagent with a reviewer prompt reads the diff cold. Third, a search that would flood the main context. Asking Claude to grep across a large repo and read twenty files costs you that context for the rest of the session. A subagent pays it and returns two paragraphs.

Claude Code ships with built-in subagents. As of September 2026 the docs list Explore (read-only, fast codebase search), Plan (read-only research during plan mode, which lesson 2 covered), and general-purpose (every tool, for multi-step tasks). Claude picks these on its own when a task fits.

Writing your own: one Markdown file

A custom subagent is a Markdown file with YAML frontmatter, the same shape as the skill files from lesson 5. Save it in .claude/agents/ for one project (commit it, the team gets it) or ~/.claude/agents/ for every project on your machine. Plugins can bundle agents too, and the docs list a managed location for organizations. Both folders are scanned recursively, so you can sort agents into subfolders. Identity comes from the name field, not the filename. Here is the docs' example:

---
name: code-reviewer
description: Reviews code for quality and best practices
tools: Read, Glob, Grep
model: sonnet
---

You are a code reviewer. When invoked, analyze the code and provide
specific, actionable feedback on quality, security, and best practices.

Only name and description are required. The description matters more than it looks: Claude reads it to decide when to delegate, so "Reviews code for quality" is a trigger, not a label. The optional fields you will actually reach for, all verified against the docs this month:

  • tools and disallowedTools: an allowlist or a denylist. Omit both and the agent inherits everything. A reviewer with only Read, Glob, Grep cannot "fix" what it was asked to judge.
  • model: sonnet, opus, haiku, inherit, or a full model ID. Route bulk search to a cheaper model and keep the expensive one for the main conversation.
  • permissionMode and maxTurns: the same mode names as lesson 2, plus a cap on how many turns the agent may take before it stops.
  • skills: preload named skills into the agent's context so it starts with your procedures loaded.
  • memory: user, project, or local. Gives the agent its own persistent memory folder across sessions. The docs recommend project.
  • hooks: yes, a subagent can carry its own hooks, which run only while it runs. Hold that thought for the second half.

The docs list more (isolation: worktree, background, color, effort, omitClaudeMd), but the six above cover the first month. To call an agent by name, type @ and pick it from the typeahead. The docs give the literal form @"code-reviewer (agent)" review the auth module, which guarantees delegation instead of leaving it to Claude's judgment. As of version 2.1.198 the /agents command no longer opens a creation wizard; it prints a reminder to ask Claude or edit the folder directly. Ask Claude to write the file.

What does not carry over. A subagent does not see your conversation history. It gets only the delegation message Claude writes for it, plus CLAUDE.md files, a git status snapshot, and any preloaded skills. The main conversation's auto memory (lesson 3) is not loaded. The docs are explicit: to give a subagent memory of its own, use the memory field. The one exception is a fork, which inherits the parent conversation instead of starting fresh, including its exact tool pool. A fork is for a side task that needs to know what you were just talking about. Every other subagent is a stranger with a job description, and that is usually what you want.

Hooks: commands that run whether Claude remembers or not

A hook is a shell command that Claude Code runs at a fixed event in the session. The docs describe it as deterministic control: certain actions always happen rather than relying on the model to choose to run them. Tap through the timeline below. Each stop is an event, and each event has a name you will put in a settings file.

Tap an event on one Claude Code turn to see the hook that fires there

When it fires

The moment a session begins, resumes, is cleared, or comes back from compaction. The matcher picks which of those: startup, resume, clear, compact, or fork.

What the hook can do

Add contextRun a command

Cannot block. Exit code 2 is ignored because the session has already started.

One concrete example

Print the last five commits and any uncommitted files so Claude starts every session knowing where you left off.

git log --oneline -5; git status --short

Events, matchers, and powers are from the Claude Code hooks reference as of September 2026. Three of the six examples are built out below; the rest are illustrative.

Those six are the events most people use. The full list from the hooks guide as of September 2026, grouped by phase:

Session:      SessionStart, Setup, SessionEnd
Prompt:       UserPromptSubmit, UserPromptExpansion
Tools:        PreToolUse, PermissionRequest, PermissionDenied,
              PostToolUse, PostToolUseFailure, PostToolBatch
Output:       Notification, MessageDisplay
Subagents:    SubagentStart, SubagentStop, TeammateIdle
Tasks:        TaskCreated, TaskCompleted
Stopping:     Stop, StopFailure
Config:       InstructionsLoaded, ConfigChange
Environment:  CwdChanged, DirectoryAdded, FileChanged,
              WorktreeCreate, WorktreeRemove
Compaction:   PreCompact, PostCompact
Model:        PreModelSwitch, PostModelSwitch
MCP input:    Elicitation, ElicitationResult

You do not need most of these yet. You need to know the list exists so that when you think "I wish it would always do X when Y," you check it before writing a note.

Where hooks live. A hook is a hooks block in a settings file. ~/.claude/settings.json applies to all your projects and stays on your machine. .claude/settings.json applies to one project and can be committed so the team gets it. .claude/settings.local.json is one project, gitignored. A plugin ships hooks in hooks/hooks.json, and both skills and subagents can carry hooks in their frontmatter. Lesson 9 covers which settings file wins when two disagree.

Matchers. Inside an event, a matcher narrows when the hook fires. For tool events the matcher is the tool name: Bash, or Edit|Write for either, or a regex like mcp__memory__.* for every tool from one MCP server. For SessionStart it is how the session started: startup, resume, clear, compact, or fork. For SubagentStop it is the agent type. An empty matcher, or *, means every time. UserPromptSubmit and Stop take no matcher at all.

JSON in, exit code out. When an event fires, Claude Code pipes a JSON object into your command's standard input. Every event includes session_id, cwd, and hook_event_name. Tool events add tool_name and tool_input, so a Bash hook can read the exact command at .tool_input.command and an Edit hook can read the file at .tool_input.file_path. The docs use jq, a small command-line JSON tool, to pull those fields out. Your script then answers with its exit code. Exit 0 means no objection; for PreToolUse the normal permission flow still applies. Exit 2 means block, and whatever you wrote to standard error becomes the reason. On PreToolUse that cancels the tool call and Claude reads your reason as feedback. On PostToolUse it stops the tool's result from reaching Claude, but the tool already ran. On SessionStart exit 2 is ignored, because the session has already started. Any other exit code is a non-blocking error; the action proceeds and the transcript shows a hook error notice. For finer control, exit 0 and print JSON instead: on PreToolUse a permissionDecision of allow, deny, or ask inside hookSpecificOutput. The docs say pick one approach per hook, exit 2 or JSON, not both.

One more property worth memorizing: PreToolUse hooks fire before the permission check, in every mode from lesson 2, including bypass. A hook that returns deny blocks the tool even when someone launched Claude with permissions skipped. Hooks can tighten what settings allow. They cannot loosen it. To see what is configured, type /hooks. It is a read-only browser as of September 2026: every event, a count of hooks on each, and the source file for every one. Editing happens in the JSON, or by asking Claude.

Three hooks worth copying

All three are command hooks. Each JSON block goes in .claude/settings.json at the project root, or in your user settings if you want it everywhere. If your file already has a hooks key, add the new event as a sibling inside it rather than pasting a second hooks object. All three use jq; install it with brew install jq on a Mac.

1. Block rm -rf outside the project (PreToolUse). The docs' own example blocks every rm -rf. This version is narrower: it lets Claude delete a build folder by relative path and blocks any target that starts with /, ~, or ... Save the script, make it executable with chmod +x, then register it.

#!/bin/bash
# .claude/hooks/block-rm.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')

# A target that starts with /, ~, or .. reaches outside the project.
if echo "$COMMAND" | grep -Eq 'rm -rf +(/|~|\.\.)'; then
  echo "Blocked: rm -rf outside the project. Use a relative path inside the repo, or ask." >&2
  exit 2
fi

exit 0
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}

$CLAUDE_PROJECT_DIR is an environment variable Claude Code sets to the folder the session started in. When the hook fires, Claude sees the Blocked: line and tries a different approach.

2. Run the formatter after every edit (PostToolUse). This is the docs' example almost verbatim. The matcher limits it to Edit and Write, so it does not fire after every Bash command. Swap prettier for whatever your project uses.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

3. Print where you left off at session start (SessionStart). Anything the hook writes to standard output becomes context Claude can see. Resist the urge to echo a fixed sentence here; the docs say a static reminder on every start belongs in CLAUDE.md, and they are right, that is a note. A hook earns its place by printing something a file cannot: live state. This one shows the last five commits and any uncommitted files, and it runs on a fresh start or a resume, not after every /clear.

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Where you left off:'; git log --oneline -5; git status --short"
          }
        ]
      }
    ]
  }
}

The docs' own version uses the compact matcher to re-inject a reminder after compaction, the one case where a fixed sentence is defensible because the summary may have dropped it.

Try this yourself

Add the formatter hook to a project you already use with Claude Code, then prove it fires. Two ways to verify, and you should do both once.

1. Open .claude/settings.json in your project root (create it if
   missing) and paste hook 2 above. Replace prettier with your
   formatter if the project uses a different one.

2. Start Claude Code in that folder and type /hooks. Select
   PostToolUse. You should see one entry with the matcher Edit|Write
   and the source Project Settings. Press Esc.

3. Ask Claude: "Add a comment at the top of <some file> using single
   quotes around a word." Open the file. With Prettier's defaults the
   quotes are now double quotes, and Claude said nothing about it.
   That silence is the hook working.

If step 3 shows no change, run claude --debug and look for the hook in the debug log. The usual cause is a script that is not executable or a settings file with a JSON typo. Next lesson: which settings file wins when two of them disagree, and how to keep API keys out of every one of them.

Next lesson9. Settings and secrets

Last updated September 17, 2026