Claude Code 2.1 Hooks Tutorial: Build Safety, Automation & Notifications

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 EventWhen It FiresUse Case
PreToolUseBefore any tool executesBlock dangerous commands, validate inputs
PostToolUseAfter a tool completesAuto-format, lint, log changes
StopWhen Claude finishes respondingSend notifications, cleanup
UserPromptSubmitWhen user sends a promptInput validation, logging
PermissionRequestWhen Claude asks for tool permissionCustom approval workflows
SubagentStopWhen a subagent finishesAggregate results, cleanup
NotificationWhen Claude sends notificationsCustom notification routing
SessionEndWhen the session terminatesFinal 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\"'"
          }
        ]
      }
    ]
  }
}
Claude Code hooks workflow diagram showing lifecycle events and automation triggers

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.

Get the Daily Pulse

Sharp AI analysis, daily. Two minutes, every morning.

Get the Daily PulseTwo minutes, every morning