Claude Code 2.1 dropped on January 7, 2026, with 1,096 commits and one feature that changes everything: agent lifecycle hooks. Think of them as programmable tripwires that fire at exactly the right moments—before a command runs, after a file is edited, when a task completes. No more hoping Claude remembers to format your code. No more crossing your fingers that it won’t run something dangerous.
Hooks make AI agent behavior deterministic. And in this tutorial, you’ll learn how to set them up, write custom hook scripts, and build practical automations that actually work.
What are Claude Code hooks?
Hooks are shell commands that execute at specific points in Claude Code’s lifecycle. They’re not suggestions or prompts—they’re guaranteed to run. This is the difference between writing “always run prettier after editing” in your CLAUDE.md (which Claude might forget) versus having a PostToolUse hook that formats code every single time, without fail.
Claude Code 2.1 supports eight hook events:
| Hook Event | When It Fires | Use Case |
|---|---|---|
| PreToolUse | Before any tool executes | Block dangerous commands, validate inputs |
| PostToolUse | After a tool completes | Auto-format, lint, log changes |
| Stop | When Claude finishes responding | Send notifications, cleanup |
| UserPromptSubmit | When user sends a prompt | Input validation, logging |
| PermissionRequest | When Claude asks for tool permission | Custom approval workflows |
| SubagentStop | When a subagent finishes | Aggregate results, cleanup |
| Notification | When Claude sends notifications | Custom notification routing |
| SessionEnd | When the session terminates | Final cleanup, statistics |
The most useful hooks for day-to-day work are PreToolUse, PostToolUse, and Stop. Let’s build all three.
Setting up your hooks directory
First, create the hooks directory in your project:
mkdir -p .claude/hooks
touch .claude/settings.json
Hooks are configured in .claude/settings.json (project-level) or ~/.claude/settings.json (global). Project settings override global settings, so you can have different hooks for different repos.
Example 1: Block dangerous commands
Let’s start with a PreToolUse hook that prevents Claude from running commands that could wreck your system. This fires before any Bash command executes, checks for dangerous patterns, and blocks with exit code 2 if found.
Create .claude/hooks/block-dangerous.sh:
#!/usr/bin/env bash
set -euo pipefail
# Read JSON input from stdin
json_input=$(cat)
command=$(echo "$json_input" | jq -r '.tool_input.command // empty')
# Dangerous patterns to block
dangerous_patterns=(
"rm -rf /"
"rm -rf ~"
"sudo rm"
"chmod 777"
"mkfs"
"dd if=/dev"
)
for pattern in "${dangerous_patterns[@]}"; do
if [[ "$command" == *"$pattern"* ]]; then
echo "BLOCKED: Dangerous command pattern detected: $pattern" >&2
exit 2
fi
done
exit 0
Make it executable: chmod +x .claude/hooks/block-dangerous.sh
Then add to .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/block-dangerous.sh",
"timeout": 5
}
]
}
]
}
}
The matcher field specifies which tools trigger this hook. Use "Bash" for exact match, "Edit|Write" for multiple tools, or "*" for all tools. The timeout prevents hung scripts from blocking Claude indefinitely.
Example 2: Auto-format TypeScript after edits
PostToolUse hooks run after a tool completes. This one automatically formats TypeScript files whenever Claude edits them—no more reminding it to run Prettier.
Create .claude/hooks/format-typescript.sh:
#!/usr/bin/env bash
set -euo pipefail
json_input=$(cat)
file_path=$(echo "$json_input" | jq -r '.tool_input.file_path // empty')
# Only format TypeScript files
if [[ "$file_path" =~ \.(ts|tsx)$ ]]; then
echo "Formatting: $file_path"
npx prettier --write "$file_path" 2>/dev/null || true
fi
exit 0
Add to your settings:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/format-typescript.sh"
}
]
}
]
}
}
Now every file edit triggers auto-formatting. The || true ensures Prettier errors don’t block Claude—non-critical hooks should fail gracefully.
Example 3: Slack notification when task completes
Stop hooks fire when Claude finishes responding. Perfect for notifications when you’re away from your terminal.
Create .claude/hooks/notify-slack.sh:
#!/usr/bin/env bash
set -euo pipefail
SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}"
if [ -z "$SLACK_WEBHOOK_URL" ]; then
exit 0
fi
json_input=$(cat)
session_id=$(echo "$json_input" | jq -r '.session_id // "unknown"')
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-type: application/json' \
--data "{
\"text\": \"Claude Code task completed\",
\"blocks\": [{
\"type\": \"section\",
\"text\": {
\"type\": \"mrkdwn\",
\"text\": \"*Claude Code Task Completed*\nSession: \`$session_id\`\"
}
}]
}"
exit 0
Set your webhook URL as an environment variable: export SLACK_WEBHOOK_URL="https://hooks.slack.com/..."
For macOS users who want native notifications instead, this one-liner works without any setup:
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code finished\" with title \"Task Complete\" sound name \"Glass\"'"
}
]
}
]
}
}

Advanced: Modifying inputs with PreToolUse
PreToolUse hooks can do more than block—they can modify tool inputs before execution. New in Claude Code 2.0.10, this lets you inject safety flags automatically.
Here’s a hook that adds --dry-run to npm publish commands:
#!/usr/bin/env bash
set -euo pipefail
json_input=$(cat)
command=$(echo "$json_input" | jq -r '.tool_input.command // empty')
if [[ "$command" == *"npm publish"* ]] && [[ "$command" != *"--dry-run"* ]]; then
modified="${command} --dry-run"
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Added --dry-run for safety",
"updatedInput": {
"command": "$modified"
}
}
}
EOF
fi
exit 0
The magic is in hookSpecificOutput.updatedInput—this replaces the original tool input with your modified version. Claude sees the change and proceeds with the safer command.
Other Claude Code 2.1 features worth knowing
Beyond hooks, this release includes several quality-of-life improvements. Hot reload for skills means you can add or edit skills in .claude/skills without restarting Claude Code. Session teleportation via /teleport lets you move sessions between CLI and claude.ai/code. Agent-scoped hooks let you define PreToolUse and PostToolUse hooks directly in agent frontmatter, so they only run during that specific agent’s lifecycle.
There’s also a 3x memory improvement for large conversations, the new LSP tool for code intelligence (go-to-definition, find references), and MCP wildcard permissions (mcp__server__*) for cleaner bulk tool access.
Debugging hooks
When hooks don’t work as expected, test them manually:
# Test with sample input
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | .claude/hooks/block-dangerous.sh
echo $? # Should be 2 (blocked)
Exit codes matter: 0 means success (proceed), 2 means block (PreToolUse only), and any other code shows a warning but doesn’t stop execution.
The bottom line
Claude Code hooks solve a fundamental problem with AI agents: they’re probabilistic, not deterministic. You can tell Claude to always format code or never run dangerous commands, but instructions can be forgotten or misinterpreted. Hooks guarantee behavior. They execute every time, exactly when you specify, regardless of what Claude is thinking about.
Start with the three examples above—safety blocking, auto-formatting, and notifications. Once those work, explore input modification and agent-scoped hooks. The official documentation has additional examples, and the hooks mastery repo on GitHub is worth bookmarking.
AI agents are only as reliable as the guardrails around them. Build yours.
Get the Daily Pulse
Sharp analysis on what's actually moving in AI. No hype, no filler, no weekly digest.



