All articles Day 9 · Extending it

Delegate the work.
Automate the rules.

By Kowshik Kesavarapu 5 min quick start + 34 min full guide Oct 20, 2026

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.

Part 1: SubagentsSections 02 to 09. Hand a job to a helper with its own context, tools and model. Built-in helpers, your own helpers, running them in parallel and in the background.
Part 2: HooksSections 10 to 26. Rules that run every time: notifications, auto-format, protected files, blocked commands, context re-injection, and smarter checks that use a model.
Both in VS CodeSection 27. Everything is plain files and settings, so the terminal and the extension share it.

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:

Protect your context"Find every place we call the payments API" can mean reading forty files. Done in your main chat, that fills the window with search noise and Claude starts forgetting your real task (Day 5). A subagent reads the forty files and returns a ten-line answer.
Limit what a helper can doA reviewer should read code, not change it. You give it read-only tools, so it cannot edit by accident. This is a guardrail you set once in the file.
Fit the model to the jobA quick search does not need your most powerful (and most expensive) model. A subagent can run on a small fast model while the main session keeps the strong one.
Work in parallelClaude can start several subagents at the same time, for example one per module, then combine the answers. Up to 20 can run at once by default.

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.

ExploreFast and read-only. Built for searching and understanding a codebase. Claude sets a thoroughness level: quick, medium or very thorough. Starts without your CLAUDE.md or the git status, so it stays small.
PlanRead-only research used in plan mode (Day 4) to gather what Claude needs before it writes a plan. Also starts without CLAUDE.md.
general-purposeFull tools. For multi-step jobs that need both exploring and changing things. The default when nothing more specific fits.
OthersSmall helpers such as 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:

>_Ask Claude
—☐✕
> 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:

>_Create it
—☐✕
mkdir .claude\agents
code .claude\agents\code-reviewer.md
>_.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:

name, descriptionThe name is the identifier. The description is what Claude reads to decide when to delegate, so write it like a skill description (Day 8): what it does, and when to use it. Adding "use proactively" nudges Claude to delegate without being asked.
tools, disallowedToolsAn allowlist or a denylist of tools. Leave both out and the helper inherits everything. For a reviewer or researcher, list only Read, Grep, Glob. This is your main safety control.
modelsonnet, opus, haiku, a full model ID, or inherit to match the main session. Use a smaller model for cheap, simple jobs.
permissionModeHow the helper handles permission prompts, for example plan for read-only planning or acceptEdits. Same ideas as Day 5.
skillsSkills whose full text is loaded into the helper at start, so it knows your procedures without discovering them.
memoryuser, project or local gives the helper memory that persists between runs, so a reviewer can learn your codebase over time.
mcpServersConnect an MCP server (Day 7) for this helper only, so its tools do not clutter your main session.
hooksHooks that run only while this helper is working (Part 2).
maxTurns, effortCap how many steps it may take, and how hard it thinks. Good for keeping cheap jobs cheap.
isolationworktree runs the helper in a temporary copy of the repo (a git worktree) so its edits cannot collide with yours.
backgroundtrue always runs it in the background.
colorA label colour so you can tell helpers apart in the interface.

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:

1. Managed settingsSet by an organisation for everyone. You will only meet this at work.
2. --agents flagDefined on the command line for one session only (next section).
3. .claude/agents/Inside the project. Commit it and teammates get the same helpers.
4. ~/.claude/agents/Your personal helpers, available in every project. On Windows: $HOME\.claude\agents\.
5. PluginsHelpers shipped in a plugin. Lowest priority. Plugins get their own day.

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:

Let Claude decideDescribe the job, or just work. If the task matches a helper's description, Claude delegates on its own.
Ask in wordsuse the code-reviewer agent to check the auth changes. Claude still chooses the exact instructions it sends.
@-mentionType @ and pick the helper, for example @"code-reviewer (agent)" look at the auth changes. This guarantees that helper is the one that runs.
Whole session as a helperclaude --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 session-only subagent
—☐✕
$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

Foreground vs backgroundA foreground helper blocks your chat until it finishes, and its permission prompts come to you. A background helper runs alongside while you keep working, and its prompts appear in your main session with its name on them. In the terminal press Ctrl+B to push a running task to the background. In VS Code use Run in background.
ResumingEach call starts a fresh helper. To continue earlier work, ask Claude to resume it (it uses the helper's ID or name). A resumed helper keeps its full history. Explore and Plan cannot be resumed.
Context isolationA normal helper does not see your conversation or the files you read earlier. It only gets the task text Claude writes, its own prompt, CLAUDE.md and any preloaded skills. So a vague delegation produces a vague result: tell Claude exactly what to hand over.
Nesting and countHelpers can start their own helpers, up to three layers below your main session by default. Up to 20 can run at once. To stop one from spawning more, leave Agent out of its tools.
TranscriptsEvery helper's full transcript is saved under ~/.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:

Parallel researchUse 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.
Second opinionAfter 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.
Noisy commandsUse a subagent to run the full test suite and report only the failures. The thousand lines of passing output never reach your context.
Read-only auditA helper with tools: Read, Grep, Glob and a security-minded prompt can scan the whole repo with zero chance of editing it.
Safe experimentsA helper with isolation: worktree can try a risky refactor in a throwaway copy. If it fails, nothing of yours changed.
ChainsReviewer finds problems, then a fixer helper repairs them. Claude can chain them in one request, or you can run them one after another.

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 momentCalled an event. Examples: "before a tool runs", "after a file is edited", "Claude finished".
The filterCalled a matcher. "Only when the tool is Bash", "only when the tool is Edit or Write".
The actionCalled a handler. Usually a command: a one-liner or a script you wrote. It can look at what is about to happen and say go ahead, or stop.
The answerAn exit code or a small piece of JSON. "0" means fine, "2" means block, and what you wrote to the error output becomes the explanation Claude reads.

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.

>_The life of one request
—☐✕
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.

>_.claude/settings.json
—☐✕
{
  "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.)

Lines 1 and 2The top-level hooks object holds one key per event. Add more events as siblings, never a second hooks object.
Line 3The matcher picks which tool, session type or notification type this applies to. Empty or missing means "every time".
Line 4Each event holds a list of matcher groups, and each group holds a list of hooks. So you can run several commands for one match. They run in parallel.
Lines 5 and 6What to actually do. A short one-liner can live right in the 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.

>_Windows (PowerShell) — settings.json
—☐✕
{
  "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')\""
          }
        ]
      }
    ]
  }
}
>_Linux — settings.json (macOS: use osascript)
—☐✕
{
  "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:

SessionStartA session begins or resumes. The matcher can be startup, resume, clear or compact. Anything your command prints is added to Claude's context.
UserPromptSubmitYou press Enter, before Claude reads the prompt. Add context to every prompt, or block it.
PreToolUseBefore a tool runs. The one that can say no. Block dangerous commands and protected files here.
PermissionRequestClaude is about to ask you to approve something. A hook can answer for you, for narrow cases you always allow.
PostToolUseAfter a tool succeeds. Format, lint, log. It cannot undo the action, since it already happened.
NotificationClaude is waiting on you.
StopClaude finishes responding. Run a final check, or tell Claude to keep going. Not fired when you interrupt.
SubagentStart, SubagentStopA helper from Part 1 starts or finishes.
PreCompact, PostCompactAround context compaction (Day 5).
FileChanged, CwdChangedA watched file changes, or Claude changes directory. Good for reloading environment variables.
SessionEndThe session closes. Clean up temporary files.

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.

Tool eventsFor 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.
SessionStartMatches how it started: startup, resume, clear, compact.
NotificationMatches the type, such as permission_prompt or idle_prompt.
Subagent eventsMatch the helper name: Explore, Plan or your own.
No matcher supportUserPromptSubmit 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:

>_Run only for git commands
—☐✕
{
  "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:

>_Input your script receives (stdin)
—☐✕
{
  "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:

Exit 0No objection. The action goes ahead through the normal permission flow. (On a PreToolUse hook, exit 0 does not mean approved.) For SessionStart and UserPromptSubmit, anything printed to stdout is added to Claude's context.
Exit 2Block. Whatever you wrote to stderr goes back to Claude as the reason, so it can change approach. Works on events that can be blocked, like PreToolUse.
Any other codeA non-blocking error. The action continues and you see a hook error notice.
JSON on stdoutFor finer control, exit 0 and print JSON instead. Pick one style per hook, not both.
>_A JSON decision from a PreToolUse hook
—☐✕
{
  "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+`:

>_Check the tools
—☐✕
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:

>_.claude/settings.json
—☐✕
{
  "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:

>_Test without Claude
—☐✕
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:

>_.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:

>_.claude/settings.json
—☐✕
{
  "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:

>_.claude/hooks/protect_files.py
—☐✕
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:

>_Block a risky command (shell script)
—☐✕
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:

>_.claude/settings.json
—☐✕
{
  "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

Log every commandPostToolUse with matcher Bash and jq -r '.tool_input.command' >> ~/.claude/command-log.txt gives you an audit trail.
Audit config changesThe ConfigChange event fires when a settings or skills file changes, so you can log or block it.
Reload environmentSessionStart plus CwdChanged hooks that run direnv export bash > "$CLAUDE_ENV_FILE" keep environment variables right as directories change.
Auto-approve one promptA PermissionRequest hook with matcher ExitPlanMode can approve the "proceed with this plan" prompt. Keep the matcher narrow: an empty one would approve everything.
Clean upSessionEnd 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.

promptSends your question plus the event data to a model for a single decision, answered as {"ok": true} or {"ok": false, "reason": "..."}. Cheap and fast.
agentStarts a helper (Part 1) that can read files and run commands to check something for real, for up to 50 steps. Experimental, so prefer command hooks for production.
httpPosts the event JSON to a URL, for a shared team service such as an audit log. Needs allowedEnvVars for any secrets in headers.
>_A Stop hook that checks the work is finished
—☐✕
{
  "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:

~/.claude/settings.jsonAll your projects, only you. Good for notifications.
.claude/settings.jsonOne project, committed, shared with the team. Good for formatting and protected files.
.claude/settings.local.jsonOne project, just you, not committed.
Managed settingsOrganisation-wide, set by admins.
Plugin hooks/hooks.jsonActive while that plugin is enabled.
Skill or subagent frontmatterThe hook lives only while that skill or helper is active. A reviewer helper can carry its own checks.

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:

>_.claude/agents/db-reader.md
—☐✕
---
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

Not firingRun /hooks and check the hook sits under the right event. Matchers are case-sensitive. Confirm you chose PreToolUse (before) versus PostToolUse (after).
Hook error in the transcriptTest your script by hand: 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.
No hooks listedSettings JSON must be valid: no trailing commas, no comments. The file must be at .claude/settings.json or ~/.claude/settings.json. Restart the session if the watcher missed the change.
JSON has no effectSomething else is printing before your JSON (often a line in your shell profile), or a field is at the wrong level. Run claude --debug and search the log. Guard profile echoes with if [[ $- == *i* ]].
Stop hook loopsA 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.
See what happenedPress Ctrl+O for the transcript view. Success is silent; blocks and errors show a message.
WindowsHook commands run through Git Bash, so install Git for Windows if you see a "Git Bash wasn't found" message.

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.

SubagentsAsk in the Claude panel to create or use one, or use @ to pick a helper. For a running task use Run in background. Open a new conversation after changing fork settings.
Edit the filesCreate .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.
Hook scriptsKeep them in .claude/hooks/. The built-in terminal (Ctrl+`) is the easiest place to test a script by piping sample JSON into it.
PermissionsThe extension answers permission prompts itself, so timing for permission_prompt notifications differs from the terminal. Test the hook in the place you actually work.
Check itType /hooks in the panel to browse what is configured.

28Habits that keep both useful

Subagent: one job each"Review security" is a helper. "Do everything" is not. Narrow helpers are chosen correctly and are easy to fix.
Least toolsGive a helper only the tools it needs. Reviewers and researchers rarely need Edit or Write.
Write the description wellIt decides when Claude delegates. Say what and when, in the words you really use.
Delegate big, not smallUse helpers for large, independent or noisy work. Do tiny edits yourself.
Hook: start harmlessNotifications first, then formatting, then blocking. Add power slowly.
Test with sample JSONPipe a fake event into your script and read the exit code before you trust it.
Fast and quietHooks run on every matching event. A slow script slows every edit.
Commit the shared onesProject hooks and helpers are code. Review changes to them in a PR.

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.

"Tell me when Claude needs me"Notification, matcher empty or permission_prompt.
"Format or lint after edits"PostToolUse, matcher Edit|Write.
"Never touch this file"PreToolUse, matcher Edit|Write, exit 2.
"Never run this command"PreToolUse, matcher Bash, exit 2.
"Keep a log of what ran"PostToolUse, matcher Bash, append to a file.
"Add facts to every prompt"UserPromptSubmit, print text or return additionalContext.
"Remind Claude after compaction"SessionStart, matcher compact.
"Check the work before it stops"Stop, a prompt or agent hook.
"Skip one approval prompt I always click"PermissionRequest, narrow matcher, JSON allow.
"Clean up when I close"SessionEnd.

30Questions beginners ask about hooks

Do hooks cost tokens?A command hook is an ordinary program, so it makes no model call. Only text your hook prints back for certain events (like SessionStart) is added to the context, so keep that short. Prompt and agent hooks do use a model.
Can Claude see my hooks?It sees their results: a block message, or text you print for it. It cannot skip a hook, because Claude Code runs it, not Claude.
Are hooks safe?They are exactly as safe as the command inside. They run with your permissions on your machine, so read hooks in any repo you clone before you trust it.
Hook or CLAUDE.md?CLAUDE.md for guidance ("prefer small functions"). A hook for rules that must never fail ("never edit .env"). A deny rule in permissions (Day 5) is the simplest way to block a plain tool call without a script.
Why does my hook run but nothing happens?Exit 0 with no output means "no objection". A PreToolUse hook only blocks on exit 2, or on JSON with permissionDecision: "deny".
Can one hook break my session?A slow hook delays every matching action, and a command that errors shows a hook error. Failures usually do not stop Claude, apart from the cases where you set a hook to block on failure. Test a new hook on a throwaway project first.

31Try this today

NotifyAdd the Notification hook for your system, then start a task that needs approval and switch windows.
ProtectAdd the protect-files hook and ask Claude to edit your .env. Watch it get refused.
DelegateCreate a read-only code-reviewer helper, make a small change, then run use the code-reviewer agent on my changes.
CompareAsk Claude to explore a part of your code twice: once in your main chat, once through an Explore helper. Notice how much cleaner the context is.

Next up: plugins. How to bundle skills, subagents, hooks and MCP servers into one package you can share and install anywhere.

Follow along

A new article lands every day. I post the short version on LinkedIn.