·Skills & Commands
How to run scripts at Claude Code lifecycle events with hooks (PreToolUse, PostToolUse, Stop) and spin up specialized parallel agents with subagents — with real configuration and the patterns that hold up in daily use.
Claude Code Hooks and Subagents: Deterministic Automation and Parallel Work
Two features turn Claude Code from a smart chat tool into something you can rely on for repeatable work: hooks and subagents. Hooks let you run your own scripts at fixed points in the Claude Code lifecycle, so deterministic side effects — formatting, validation, notifications, blocking dangerous commands — happen every time without the model remembering to do them. Subagents let you spin up specialized agents with their own context and tool budget, so you can parallelize independent work and keep the main thread focused. This guide covers both, with real configuration and the patterns that hold up in daily use.
What hooks are for
A hook is a shell command Claude Code runs at a named lifecycle event. The model does not decide whether to run it — the host guarantees it. That is the whole point: anything you must happen on every tool call, or every edit, or every session end, belongs in a hook rather than in a prompt you hope the model remembers.
The events that matter most day to day:
- PreToolUse — fires before a tool runs. Lets you inspect or block the call.
- PostToolUse — fires after a tool completes. Lets you validate, format, or feed context back to the model.
- UserPromptSubmit — fires when the user submits a prompt. Useful for logging or injecting context.
- Stop — fires when Claude finishes responding. Good for notifications.
- SubagentStop — fires when a subagent finishes, the subagent analogue of Stop.
- Notification — fires when Claude Code raises a system notification.
- SessionStart / SessionEnd — fire at session boundaries.
- PreCompact — fires before context compaction.
There are a handful more (Setup, PermissionRequest, and failure variants), but the ones above cover almost every real use.
Configuring hooks
Hooks are configured in settings.json (project-level in .claude/settings.json, user-level in ~/.claude/settings.json). The structure is a map from event name to an array of matcher groups, each holding a list of commands:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "prettier --write \"$CLAUDE_PROJECT_DIR/$tool_input.file_path\" 2>/dev/null || true"
}
]
}
]
}
}
The matcher is a regex tested against the tool name (for PreToolUse/PostToolUse) or left empty to match every tool. type is always "command". The command is a shell string run by the host. Use $CLAUDE_PROJECT_DIR to anchor paths to the project root — relative paths are unreliable because the working directory is not always what you expect.
Each hook receives its context as a JSON object on stdin. The fields vary by event but always include session_id, timestamp, and cwd. Tool events add tool_name, tool_input (the arguments), and tool_use_id; PostToolUse also includes tool_response. A hook reads stdin, decides what to do, and signals the result in one of two ways: exit code, or JSON on stdout.
A PreToolUse guard
The most common PreToolUse pattern is a guard: block a tool call if it matches a rule the model should not be allowed to violate. Here is a script that blocks any rm -rf invocation:
#!/usr/bin/env bash
# .claude/hooks/block-rmrf.sh
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')
if echo "$cmd" | grep -q 'rm -rf'; then
cat <<EOF
{
"decision": "block",
"reason": "rm -rf is blocked by project policy. Use a more specific cleanup command."
}
EOF
exit 0
fi
# Approve (or just exit 0 with no output to let it through)
exit 0
Wire it up in settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash $CLAUDE_PROJECT_DIR/.claude/hooks/block-rmrf.sh"
}
]
}
]
}
}
Now every Bash tool call passes through the script first. If the script returns {"decision": "block", ...}, the call is prevented and the reason is shown to Claude so it can adapt. If it returns nothing or {"decision": "approve"}, the call proceeds. A PreToolUse hook can also return exit 2 as a shorthand for block. Hooks run before permission rules, so a blocking hook takes precedence even over an allow rule — a belt-and-suspenders way to enforce policy.
A PostToolUse formatter
PostToolUse hooks shine for "after the fact" automation. A classic example: reformat every file Claude writes, so the model never hands you unformatted code. The matcher targets Edit and Write, and the command runs your formatter on the affected file:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$(echo $0)\" 2>/dev/null; exit 0"
}
]
}
]
}
}
A PostToolUse hook can also return JSON to feed information back to Claude. Returning {"decision": "block", "reason": "..."} does not undo the edit, but it does prompt Claude with the reason so it can react — useful for "this file now fails typecheck, please fix it" feedback loops. The pattern of "edit → hook runs linter → hook feeds errors back → Claude fixes" is one of the most reliable ways to keep quality high without supervising every change.
Hook pitfalls
A few things bite people:
- Malformed JSON output silently fails. If your hook prints anything that is not valid JSON to stdout when it meant to emit a decision, the host ignores it. Print diagnostics to stderr, decisions to stdout, and never
echodebug noise to stdout. - Exit codes are meaningful. Exit 0 means success; exit 2 blocks (for PreToolUse); other non-zero codes surface as a hook error. A crashing hook can stall a session.
- Hooks are not sandboxed by default. They run with your full privileges. Treat a hook command the way you would treat any script you run — review it, especially when copying from the internet.
- Long-running hooks slow every tool call. A PreToolUse hook that takes two seconds makes Claude feel sluggish. Keep hooks fast; push slow work to PostToolUse or Stop.
What subagents are for
A subagent is a specialized Claude Code agent with its own system prompt, its own allowed tools, and its own context window, spawned by the main agent through the Task tool. The main agent delegates a bounded piece of work, the subagent does it in isolation, and returns a condensed result. Two things make this powerful:
- Context isolation. A subagent's reading and reasoning do not pollute the main thread's context window. If you need to read 20 files to answer a question, doing it in a subagent keeps the main thread clean.
- Parallelism. The main agent can launch multiple Task tool calls in a single response, and the subagents run in parallel — ideal for independent research or coding tasks.
Subagents shine for "fan out, then synthesize" work: investigate three possible approaches in parallel, review different parts of a codebase simultaneously, or run independent experiments. They are less useful for tightly coupled work where the main thread needs to see every intermediate step.
Defining a subagent
A subagent is a Markdown file with YAML frontmatter, placed in .claude/agents/ (project-level, checked in) or ~/.claude/agents/ (user-level):
.claude/agents/
└── code-reviewer.md
.claude/agents/code-reviewer.md:
---
name: code-reviewer
description: Reviews staged changes for bugs, security issues, and style. Use proactively before opening a PR or when the user asks for a review.
tools: ["Read", "Grep", "Bash(git diff:*)", "Bash(git log:*)"]
---
You are a meticulous code reviewer. When given a set of changes:
1. Read the full diff against the base branch.
2. Check for: unhandled error paths, secret leakage in logs, missing tests,
naming that breaks the surrounding module's conventions.
3. Report findings grouped by file. End with one of: LGTM, Minor fixes, Block.
Do not edit files. You are read-only.
The frontmatter fields:
name— the subagent identifier, used as thesubagent_typewhen the main agent invokes it.description— what the subagent does and when to use it. The main agent reads this to decide whether to delegate, so it doubles as a trigger.tools— the tool set the subagent may use. Omitting it inherits the session defaults; restricting it keeps a subagent focused and safe (a read-only reviewer cannot accidentally rewrite code).
Once the file exists, the main agent can launch it through the Task tool with subagent_type: "code-reviewer". You can also ask Claude directly: "use the code-reviewer subagent to review my staged changes."
A parallel workflow
The payoff of subagents is parallelism. Suppose you want to investigate three independent questions — "what's causing the flaky test?", "is there a library that does X?", and "how does our auth flow compare to the new spec?". Ask Claude to delegate each to a subagent:
Investigate these three questions in parallel using subagents:
1. Find the root cause of the failing test in tests/auth.spec.ts
2. Research whether there's a maintained library for parsing RFC 8989
3. Summarize how our current auth flow differs from the spec in docs/auth.md
The main agent issues three Task tool calls, the subagents run concurrently, and you get three condensed reports back — without any of the intermediate file-reading landing in your main context. This is dramatically faster and cheaper than reading everything into one thread, and it keeps the main agent's attention on synthesis rather than exploration.
Frequently asked questions
What is the difference between a Claude Code hook and a permission rule?
A permission rule decides whether a tool call is allowed to proceed (allow, deny, or ask). A hook is a script that runs at a lifecycle event and can do arbitrary work — format a file, post a notification, block a call, feed context back to Claude. Hooks run before permission rules, so a blocking PreToolUse hook takes precedence even over an allow rule. Use rules for "should this be allowed"; use hooks for "what should happen whenever this occurs."
How do I block a dangerous command with a PreToolUse hook?
Write a hook script that reads the JSON payload from stdin, inspects tool_input.command, and prints {"decision": "block", "reason": "..."} to stdout when the command matches your blocklist. Wire it under hooks.PreToolUse with a matcher of "Bash". The host runs the script before every Bash tool call; if it returns a block decision, the call is prevented and the reason is shown to Claude.
What is a Claude Code subagent and how do I create one?
A subagent is a specialized Claude Code agent with its own system prompt, allowed tools, and context window, spawned by the main agent via the Task tool. You create one by adding a Markdown file with YAML frontmatter (name, description, tools) to .claude/agents/. The main agent reads the description to decide when to delegate, then launches the subagent with subagent_type set to its name.
Can Claude Code subagents run in parallel?
Yes. The main agent can issue multiple Task tool calls in a single response, and the subagents run concurrently with isolated contexts. This is ideal for independent research or coding tasks — investigate three approaches at once, review different parts of a codebase simultaneously — and keeps the main thread focused on synthesis rather than exploration.
Official references
- Claude Code hooks reference
- Claude Code hooks guide
- Claude Code subagents (custom agents)
- Claude Code settings and permissions
- Anthropic engineering — Claude Code
This article reflects publicly available information as of July 2026; relevant APIs may evolve.