Two roadmap rows in one day: subagents (Part 1) and hooks (Part 2). They fit together. Subagents decide who does the work. Hooks decide which rules always run, no matter who does it. A reader asked for hooks, so here they are.
01The big picture: helpers and guardrails
What. Two features that make Claude Code feel less like a chat and more like a small team with house rules. A subagent is a separate Claude with its own fresh context that you hand a job to. A hook is a command of yours that Claude Code runs automatically at a fixed moment, such as right after every file edit.
Why they belong together. Everything so far (CLAUDE.md, skills, prompts) asks Claude to behave. Asking works almost always, but "almost" is not good enough for "never touch the .env file" or "always format the code". A hook does not ask, it simply runs. And a subagent keeps big, noisy jobs out of your main conversation, which protects the context you learned to manage on Day 5.
02What a subagent is, and why you want one
What. A subagent is a specialist Claude that your main Claude can hand a task to. It starts with a clean context window, works on the task using its own instructions and its own set of tools, and sends back only a short result. The files it read and the dead ends it explored never enter your main conversation.
Why. Three reasons, and each solves a real problem:
How you meet them. You already have, without noticing. When Claude says it is "searching the codebase" with an Explore agent, or when a Day 8 skill used context: fork, a subagent did the work. Now you will make your own.
03The built-in subagents
What and why. Claude Code ships with helpers so delegation works from day one. Claude picks them itself when a task fits, and you can ask for them by name. How to use: just describe the job in words, for example use an Explore agent to find where login errors are handled.
statusline-setup and claude-code-guide (answers questions about Claude Code itself) exist too. You rarely call them by hand.Explore and Plan are one-shot: they answer once and finish. If you ever want to block a built-in, add "Agent(Explore)" to permissions.deny in your settings.
04Build your own, step by step
What. A custom subagent is one Markdown file. The header (called frontmatter) configures it. The text below the header becomes its system prompt, meaning its standing instructions. Why: you get a specialist that behaves the same every time: a reviewer with your standards, a test runner that only reports failures, a researcher that never edits. How: two ways.
Way 1: ask Claude to write it. This is the easiest start. In the terminal or the VS Code panel, say:
> create a subagent called code-reviewer in .claude/agents. It should
review changes for bugs, security and missing tests, be read-only,
and report findings as a short list with file and line.
Way 2: write the file yourself. From PowerShell, or with New File in the VS Code Explorer:
mkdir .claude\agents code .claude\agents\code-reviewer.md
--- name: code-reviewer description: Reviews code changes for bugs, security issues and missing tests. Use proactively after code is written or modified. tools: Read, Grep, Glob, Bash model: sonnet --- You are a senior code reviewer. When invoked: 1. Run git diff to see the recent changes. 2. Review only the changed code. 3. Report findings in three groups: must fix, should fix, nice to have. 4. For each finding give the file, the line, the problem and a fix. Never edit files. If everything looks good, say so in one line.
Only name and description are required. Save the file and Claude Code picks it up within a few seconds, with one exception: if you created the brand-new .claude/agents folder during a session, restart the session once. Then try use the code-reviewer agent on my uncommitted changes. The review runs in its own context, and you get the summary back.
05The header, field by field
What and why. These fields decide what the helper can do, how smart it is and how it runs. Knowing them is the difference between a toy and a trustworthy helper. These are the ones you will use:
Read, Grep, Glob. This is your main safety control.sonnet, opus, haiku, a full model ID, or inherit to match the main session. Use a smaller model for cheap, simple jobs.plan for read-only planning or acceptEdits. Same ideas as Day 5.user, project or local gives the helper memory that persists between runs, so a reviewer can learn your codebase over time.worktree runs the helper in a temporary copy of the repo (a git worktree) so its edits cannot collide with yours.true always runs it in the background.Typos fail quietly: unknown fields are ignored, and a file with broken YAML or no name is skipped. If a helper does not show up, check the header first.
06Where subagents live
What and why. As with skills, the folder decides who gets the helper. A team reviewer belongs in the repo; a personal helper belongs in your home folder. How: pick the location. When two helpers share a name, the higher one in this list wins:
$HOME\.claude\agents\.Subfolders are scanned too, so you can organise agents/review/ and agents/research/. The name inside the file is what counts, not the path.
07Four ways to run a subagent
What and why. You control how much say Claude has in the delegation. How:
use the code-reviewer agent to check the auth changes. Claude still chooses the exact instructions it sends.@ and pick the helper, for example @"code-reviewer (agent)" look at the auth changes. This guarantees that helper is the one that runs.claude --agent code-reviewer starts the entire session as that helper, with its tools and prompt. Set "agent": "code-reviewer" in .claude/settings.json to make it the project default.Try one without creating a file. The --agents flag takes JSON and lives only for that session. Good for experiments. In PowerShell, put the JSON in a here-string so quotes survive:
$a = @'
{"quick-reviewer": {
"description": "Fast reviewer. Use proactively after code changes.",
"prompt": "You are a senior reviewer. Focus on bugs and security.",
"tools": ["Read", "Grep", "Glob", "Bash"],
"model": "sonnet"
}}
'@
claude --agents $a
And one more: /subtask <task> starts a fork, a helper that inherits your whole conversation so far. Use it when the side job needs the context you built, which a normal subagent does not get.
08Foreground, background, resuming and limits
Agent out of its tools.~/.claude/projects/.../subagents/, so you can read exactly what it did.09Patterns that work
What and why. Subagents pay off when the work is big, noisy or risky. Here are the shapes that earn their keep, each with the prompt to use:
Use separate subagents to investigate the auth, billing and notification modules in parallel, then summarise how they depend on each other. Three searches at once, three short answers, a clean main chat.After you finish, use the code-reviewer agent on your own changes. A fresh reviewer with no memory of your reasoning catches what the author missed.Use a subagent to run the full test suite and report only the failures. The thousand lines of passing output never reach your context.tools: Read, Grep, Glob and a security-minded prompt can scan the whole repo with zero chance of editing it.isolation: worktree can try a risky refactor in a throwaway copy. If it fails, nothing of yours changed.When not to use one. For a small edit in a file you are already looking at, a subagent is overhead: it has to rebuild context from scratch. Delegate when the job is large, independent or noisy, not for everything.
10What a hook is, and why it is different
What. A hook is a command you register to run at a specific moment in Claude Code's life: when a session starts, before a tool runs, after a file is edited, when Claude finishes, when it needs your attention. Claude Code runs it for you and passes it information about what is happening.
Why. Compare two ways to get your code formatted. With CLAUDE.md you write "always run the formatter" and hope Claude remembers on every edit. With a hook you say "after any edit, run the formatter" and the system does it, every time, whether or not Claude would have remembered. That is deterministic. Use instructions for judgement and style; use hooks for rules that must never be skipped.
How they work. Every hook has three parts: an event (when), an optional matcher (which tool or situation) and a handler (what to run). You describe them in JSON inside a settings file. When the event fires, Claude Code sends details as JSON to your command, and your command answers with an exit code.
11Hooks in plain words: a restaurant with house rules
What. Picture a restaurant. The chef (Claude) is clever and usually does the right thing. But the owner has rules that must hold even on a rushed night: "wash hands before cooking", "never serve a dish with nuts to an allergic guest", "ring the bell when an order is ready". The owner does not trust memory for these, so each rule is a checkpoint built into the kitchen: something happens at a fixed moment, and it either lets the work continue or stops it.
A hook is exactly that checkpoint. Claude Code has fixed moments (before it uses a tool, after it edits a file, when it finishes). At each moment it asks: "is there a hook here?" If yes, it runs your command and reads the answer.
The most important sentence in this article: a hook is your program, not Claude. Claude Code runs it for you at the checkpoint. Claude cannot forget it, skip it or talk its way past it. That is the whole point.
12One request, step by step: where hooks fire
What and why. Most people struggle with hooks because they cannot picture when each one runs. So follow one real request from start to finish: you type fix the typo in README. Every numbered line below is a moment where a hook could run.
1. You press Enter UserPromptSubmit your hook can add context or block the prompt 2. Claude decides to edit README.md PreToolUse your hook can ALLOW, ASK or BLOCK the edit 3. If approval is needed PermissionRequest your hook can answer the approval prompt for you Notification your hook can ping you that Claude is waiting 4. The edit happens PostToolUse your hook can format, lint or log the result 5. Claude writes its final answer Stop your hook can run a last check, or send Claude back to work
How to read it. The word Pre means "before it happens, so I can stop it". The word Post means "after it happened, so I can react but cannot undo it". If your rule is "never do X", you want a Pre hook. If your rule is "after X, always do Y", you want a Post hook. The same pattern holds for longer sessions: SessionStart runs when the session opens, SessionEnd when it closes, and subagent and compaction events sit in between.
13A hook config, line by line
What and why. The JSON looks intimidating the first time. It is only three nested boxes, and once you see them you can read or write any hook. Here is a real one with every line explained. How: read the code, then the notes under it.
{
"hooks": { // 1. the one box that holds all hooks
"PostToolUse": [ // 2. the EVENT: after a tool succeeds
{
"matcher": "Edit|Write", // 3. the FILTER: only these tools
"hooks": [ // 4. the list of things to run
{
"type": "command", // 5. run a shell command
"command": "echo Edited!" // 6. the command itself
}
]
}
]
}
}
(The // notes are for you: real settings files must not contain comments, so do not copy them in.)
hooks object holds one key per event. Add more events as siblings, never a second hooks object.matcher picks which tool, session type or notification type this applies to. Empty or missing means "every time".command. Anything longer belongs in a script file that the command calls.The three most common mistakes: a trailing comma or a comment in the JSON (the file is then ignored), a second "hooks" key instead of adding to the existing one, and a matcher in the wrong case (edit will not match Edit).
14Your first hook: a notification when Claude needs you
What and why. You start a long task, switch to another window, and Claude sits waiting for permission for ten minutes. A Notification hook pops up a message the moment it needs you. It is the safest first hook because it changes nothing. How: open ~/.claude/settings.json (on Windows, $HOME\.claude\settings.json; create it if missing) and add the block for your system.
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')\""
}
]
}
]
}
}
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "notify-send 'Claude Code' 'Claude Code needs your attention'" }
]
}
]
}
}
The Windows version opens a dialog box, which can appear behind your window, so test it. On Linux you need a desktop notification service (libnotify-bin on Ubuntu). The empty matcher fires for every notification type. Narrow it with permission_prompt (needs approval) or idle_prompt (finished and waiting). Verify with /hooks, which lists every configured hook by event. If a settings file already has a hooks key, add your event next to the others instead of replacing the object.
Shortcut: you can also just describe it to Claude: add a hook that notifies me when you need input, then check the result with /hooks.
15The events you will use most
What. There are more than thirty events. You do not need them all. These cover most real needs:
startup, resume, clear or compact. Anything your command prints is added to Claude's context.16Matchers and the if field: aim the hook
What and why. Without a matcher a hook fires on every occurrence of its event. That is wasteful and sometimes dangerous. A matcher narrows it. How: what the matcher matches depends on the event.
PreToolUse, PostToolUse and friends, the matcher is the tool name: Bash, Edit|Write (either one), or mcp__github__.* for every GitHub MCP tool. Case matters.startup, resume, clear, compact.permission_prompt or idle_prompt.Explore, Plan or your own.UserPromptSubmit and Stop always fire on every occurrence.For finer aim use if, which uses the same pattern style as permission rules (Day 5) and works on tool events only:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
}
]
}
]
}
}
The hook process now starts only when the command is a git command. Treat if as a convenience filter, not a security boundary: use the permission rules for real enforcement.
17How a hook talks to Claude Code
What. Your command receives a JSON description of the event on stdin and answers with its exit code and what it prints. Why you need this: it is how a hook knows which file was edited or which command is about to run, and how it says "allow" or "stop". How: for a Bash command about to run, stdin looks like this:
{
"session_id": "abc123",
"cwd": "C:\\dev\\myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "npm test" }
}
Edit and Write calls carry tool_input.file_path instead. Your script reads these with a JSON tool such as jq (install it first; on Windows, for example, winget install jqlang.jq) or with a few lines of Python or Node. Then it answers:
PreToolUse hook, exit 0 does not mean approved.) For SessionStart and UserPromptSubmit, anything printed to stdout is added to Claude's context.PreToolUse.{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use rg instead of grep for better performance"
}
}
permissionDecision can be deny (cancel and tell Claude why), ask (show you the normal prompt) or allow. Put the fields inside hookSpecificOutput: at the top level they are silently ignored. To add context to a prompt, return additionalContext inside hookSpecificOutput from a UserPromptSubmit hook.
18Walkthrough: build your first real hook, step by step
What and why. Claude's edits sometimes disagree with your formatter, so your diffs fill with spacing noise. We will add a hook that runs Prettier on every file Claude writes. It is a good first real hook: useful, easy to see working and safe, since it only formats. How: follow the steps in order. The same steps work for any other hook.
Step 1. Decide the event and matcher. Ask yourself the two questions. "Must it happen before or after?" After, because the file has to exist first, so PostToolUse. "Which tool?" The ones that write files, so Edit|Write.
Step 2. Check your tools exist. This hook needs jq (to pull the file name out of the event) and npx (to run Prettier). In a terminal, or the VS Code terminal opened with Ctrl+`:
jq --version npx --version # no jq? Ubuntu: sudo apt-get install jq # no jq? Windows: winget install jqlang.jq (then open a new terminal)
Step 3. Write the hook. Open .claude/settings.json in your project (create the folder and file if needed; in VS Code use New File in the Explorer). Paste:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
What the command does, in order: Claude Code hands the event to the command as JSON. jq -r '.tool_input.file_path' pulls out just the path of the file that was edited. The pipe | passes that path on, and xargs npx prettier --write runs Prettier on it.
Step 4. Make Claude Code see it. Changes to settings are usually picked up on their own. Type /hooks in Claude Code (terminal or the VS Code panel). You should see PostToolUse with your Edit|Write entry. If it is missing, check for a trailing comma, then restart the session.
Step 5. Test it with a trap. Ask: add a line to src/app.js that logs hello using single quotes. Prettier's default turns single quotes into double quotes. Open the file: if you see "hello", the hook ran. Success is silent, so the file is your proof.
Step 6. If it did not work, test the command alone. Take Claude out of the picture and feed your command a fake event. In a terminal:
echo '{"tool_input":{"file_path":"src/app.js"}}' | jq -r '.tool_input.file_path' # should print: src/app.js # then the full command: echo '{"tool_input":{"file_path":"src/app.js"}}' | jq -r '.tool_input.file_path' | xargs npx prettier --write
If this works by hand but not inside Claude Code, press Ctrl+O to open the transcript view and look for a "hook error" line, or start with claude --debug. If it fails by hand, the problem is your command, not the hook setup. Fix it there first.
Step 7. Share it. Because the file is .claude/settings.json inside the project, committing it gives every teammate the same behaviour. Use .claude/settings.local.json instead if it is only for you.
Make it your own. Swap Prettier for Black (black), gofmt, rustfmt or ESLint (npx eslint --fix). Note the limit: this fires on Edit and Write, not on files that a shell command changed.
Shortcut for beginners: you can skip hand-writing the JSON. Say add a PostToolUse hook that runs prettier on files you edit. Claude writes it, and you verify with /hooks and the test above.
19Recipe: protect files Claude must not touch
What and why. Some files should never change by accident: .env with secrets, lock files, anything inside .git. A PreToolUse hook stops the edit before it happens, and Claude receives the reason so it can try another route. How: two files. First the script, saved as .claude/hooks/protect-files.sh:
#!/bin/bash INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') # Windows paths use backslashes; normalise them FILE_PATH="${FILE_PATH//\\//}" PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/") for pattern in "${PROTECTED_PATTERNS[@]}"; do if [[ "$FILE_PATH" == *"$pattern"* ]]; then echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2 exit 2 fi done exit 0
Make it executable on macOS and Linux with chmod +x .claude/hooks/protect-files.sh. Then register it:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh" }
]
}
]
}
}
$CLAUDE_PROJECT_DIR is the project root, so the path works from any folder. Test: ask Claude to add a comment to .env. The edit is refused and the Blocked: message reaches Claude. A hook's deny holds in every permission mode, even bypass mode, which makes hooks the right tool for rules nobody should be able to switch off.
No jq or bash on Windows? Write the script in Python instead. It needs nothing but Python:
import json, sys event = json.load(sys.stdin) path = event.get("tool_input", {}).get("file_path", "").replace("\\", "/") for pattern in (".env", "package-lock.json", ".git/"): if pattern in path: print(f"Blocked: {path} matches protected pattern '{pattern}'", file=sys.stderr) sys.exit(2) sys.exit(0)
Register it with "command": "python .claude/hooks/protect_files.py" instead of the shell script path. Same event, same matcher, same exit code 2.
20Recipe: stop dangerous commands
What and why. The same idea applies to shell commands. Matching on Bash, a script can look at tool_input.command and refuse patterns you never want, such as dropping a database table. How:
INPUT=$(cat) COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command') if echo "$COMMAND" | grep -q "drop table"; then echo "Blocked: dropping tables is not allowed" >&2 # becomes Claude's feedback exit 2 # 2 = block fi exit 0
Several hooks on the same event all run in parallel and the most restrictive answer wins (deny, then defer, then ask, then allow). That lets you stack a logging hook with a blocking hook safely. One caution: a denied sibling does not stop the others from running, so do not put side effects in a hook that depends on another hook's verdict.
21Recipe: remind Claude after compaction
What and why. When a long session compacts (Day 5), small but vital details can be lost, like "use Bun, not npm". A SessionStart hook with the compact matcher re-injects them. Its stdout goes straight into Claude's context. How:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing.'"
}
]
}
]
}
}
Replace the echo with something dynamic, like git log --oneline -5. For facts that are true in every session, plain CLAUDE.md is still simpler. Use the hook for what must come back after a compaction.
22More recipes in one list
PostToolUse with matcher Bash and jq -r '.tool_input.command' >> ~/.claude/command-log.txt gives you an audit trail.ConfigChange event fires when a settings or skills file changes, so you can log or block it.SessionStart plus CwdChanged hooks that run direnv export bash > "$CLAUDE_ENV_FILE" keep environment variables right as directories change.PermissionRequest hook with matcher ExitPlanMode can approve the "proceed with this plan" prompt. Keep the matcher narrow: an empty one would approve everything.SessionEnd with matcher clear can delete scratch files.23Smarter hooks: prompt, agent and HTTP
What and why. A command hook follows a fixed rule. Sometimes you need judgement, such as "is the work really complete?". Three other handler types cover that. How: set the type.
{"ok": true} or {"ok": false, "reason": "..."}. Cheap and fast.allowedEnvVars for any secrets in headers.{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
}
]
}
]
}
}
With ok: false on a Stop hook, Claude keeps going and uses your reason as its next instruction. Swap in "type": "agent" with a prompt like "Run the test suite and verify all tests pass" and the check becomes real.
24Where hooks live
What and why. Same logic as skills and subagents: the file decides the audience. How:
To switch every hook off, set "disableAllHooks": true. Edits to settings files are normally picked up automatically.
25Subagents and hooks together
What and why. This is where the two halves meet. Use the SubagentStart and SubagentStop events (matcher: the helper name) to log or react when helpers run. Put a hooks block inside a helper's own frontmatter to give that helper a guardrail, for example a database helper whose Bash calls are checked by a script that allows only read-only queries. How:
--- name: db-reader description: Answers questions about our data using read-only queries. tools: Bash hooks: PreToolUse: - matcher: "Bash" hooks: - type: command command: "./scripts/validate-readonly-query.sh" --- You answer data questions. Only run SELECT queries.
The helper decides what to do; the hook guarantees the rule. Plugin-supplied helpers ignore hooks, mcpServers and permissionMode for security, so copy the helper into your own folder if you need those fields.
26When a hook does not work
/hooks and check the hook sits under the right event. Matchers are case-sensitive. Confirm you chose PreToolUse (before) versus PostToolUse (after).echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh, then check echo $?. Use absolute paths or $CLAUDE_PROJECT_DIR. "jq: command not found" means install jq. A script that will not run may need chmod +x..claude/settings.json or ~/.claude/settings.json. Restart the session if the watcher missed the change.claude --debug and search the log. Guard profile echoes with if [[ $- == *i* ]].Stop hook that keeps saying "not done" is overridden after eight blocks in a row. Read stop_hook_active from the input and exit 0 when it is true.Hooks run with your permissions, on your machine, and a hook can run any command. Only add hooks you understand, read project hooks before trusting a repository you cloned, and keep scripts short. A hook can cause damage as easily as prevent it.
27In VS Code
Subagents and hooks are files and settings, so the terminal and the extension share them. Create a helper or hook in one place and it appears in the other.
@ to pick a helper. For a running task use Run in background. Open a new conversation after changing fork settings..claude/agents/<name>.md and .claude/settings.json from the Explorer. Use New File; VS Code highlights the JSON and flags trailing commas, which saves you a common hook error..claude/hooks/. The built-in terminal (Ctrl+`) is the easiest place to test a script by piping sample JSON into it.permission_prompt notifications differs from the terminal. Test the hook in the place you actually work./hooks in the panel to browse what is configured.28Habits that keep both useful
29Cheat sheet: I want this, so use that
What and why. Most of the work in hooks is choosing the event. Use this table of intentions first, then read the section for details.
Notification, matcher empty or permission_prompt.PostToolUse, matcher Edit|Write.PreToolUse, matcher Edit|Write, exit 2.PreToolUse, matcher Bash, exit 2.PostToolUse, matcher Bash, append to a file.UserPromptSubmit, print text or return additionalContext.SessionStart, matcher compact.Stop, a prompt or agent hook.PermissionRequest, narrow matcher, JSON allow.SessionEnd.30Questions beginners ask about hooks
SessionStart) is added to the context, so keep that short. Prompt and agent hooks do use a model.PreToolUse hook only blocks on exit 2, or on JSON with permissionDecision: "deny".31Try this today
.env. Watch it get refused.use the code-reviewer agent on my changes.Next up: plugins. How to bundle skills, subagents, hooks and MCP servers into one package you can share and install anywhere.