Blog

Claude Code Hooks Tutorial: How to Automate Workflows with Lifecycle Events

11 Apr 2026

Claude Code Hooks Tutorial

You can tell Claude Code to run Prettier after every file edit. You can write that in your CLAUDE.md, add it to your prompt, even repeat it three times. Claude will probably do it. Probably. Most of the time.

Hooks make it guaranteed.

Hooks are shell commands that execute at specific points in Claude Code's lifecycle — before a tool runs, after a tool completes, when a session starts, when Claude sends a notification. They're deterministic. They don't depend on the model remembering your instructions. They just run.

This guide covers everything from your first hook to production-ready configurations for auto-formatting, security gates, notifications, and context injection.

How Hooks Work

Every hook has three parts:

  1. Event — the lifecycle moment that triggers the hook (e.g., PreToolUse, PostToolUse, Notification)
  2. Matcher — an optional regex filter that narrows when the hook fires (e.g., only for the Write tool, only for .ts files)
  3. Command — the shell command or script that runs

When an event fires and the matcher matches, Claude Code passes JSON context about the event to your command's stdin. Your command does its work and communicates the result back through its exit code:

  • Exit 0 — success, continue normally
  • Exit 2 — block the action (for PreToolUse hooks, this prevents the tool from executing)

Any other exit code logs a warning but doesn't block execution.

Where to Configure Hooks

Hooks live in your settings JSON files. You have three options depending on scope:

ScopeFileShared via Git?
Global (all projects)~/.claude/settings.jsonNo
Project (team-wide).claude/settings.jsonYes
Project (personal).claude/settings.local.jsonNo (gitignored)

The structure inside any of these files looks like this:

{
  "hooks": {
    "EventName": [
      {
        "matcher": "regex pattern",
        "command": "your-command-here"
      }
    ]
  }
}

You can also ask Claude itself to add hooks for you — just describe what you want and Claude will edit the settings file.

Hook Events Reference

Claude Code supports a wide range of lifecycle events. Here are the ones you'll use most often:

EventWhen It FiresCan Block?
PreToolUseBefore a tool call executesYes (exit 2)
PostToolUseAfter a tool call completes successfullyNo
NotificationWhen Claude sends a notification (e.g., waiting for input)No
SessionStartWhen a new session begins or resumesNo
StopWhen Claude finishes its responseNo
SubagentStartWhen a subagent is spawnedNo
SubagentStopWhen a subagent finishesNo
PreCompactBefore context compaction happensNo

The PreToolUse event is by far the most powerful — it's the only one that can prevent an action from happening.

Your First Hook: Auto-Format on Save

The most common hook auto-formats files after Claude edits them. No more reminding Claude to run Prettier — it happens every time, automatically.

Add this to your .claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "npx prettier --write \"$TOOL_INPUT_FILE_PATH\""
      }
    ]
  }
}

This hook fires after every Edit or Write tool call and runs Prettier on the affected file. The $TOOL_INPUT_FILE_PATH environment variable is automatically set by Claude Code to the path of the file that was just modified.

What's Happening Under the Hood

When Claude edits a file, the sequence is:

  1. Claude calls the Edit tool with the file path and changes
  2. The tool executes and modifies the file
  3. Claude Code checks PostToolUse hooks — the Edit|Write matcher matches
  4. Your command runs: npx prettier --write "src/app/page.tsx"
  5. Prettier formats the file
  6. Claude continues with the formatted version

The model never has to think about formatting. It just happens.

Security Gate: Blocking Dangerous Commands

A PreToolUse hook can inspect commands before they run and block anything dangerous. This is useful for preventing accidental destructive operations.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "bash -c 'read input; cmd=$(echo \"$input\" | jq -r \".tool_input.command\"); case \"$cmd\" in *\"rm -rf /\"*|*\"DROP TABLE\"*|*\"--force\"*|*\"--no-verify\"*) echo \"Blocked dangerous command: $cmd\" >&2; exit 2;; *) exit 0;; esac'"
      }
    ]
  }
}

This hook:

  • Matches only Bash tool calls
  • Reads the JSON input from stdin to extract the command
  • Checks for dangerous patterns like rm -rf /, DROP TABLE, --force, --no-verify
  • Exits with code 2 to block the command if a match is found
  • Exits with code 0 to allow safe commands through

When a command is blocked, Claude sees the stderr message and knows the action was prevented. It can then adjust its approach or ask you for guidance.

A Cleaner Version with a Script

For complex validation logic, use a dedicated script instead of an inline command. Create .claude/scripts/guard.sh:

#!/bin/bash

input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')

if [ -z "$command" ]; then
  exit 0
fi

blocked_patterns=(
  "rm -rf /"
  "DROP TABLE"
  "DROP DATABASE"
  "git push.*--force"
  "git reset --hard"
  "--no-verify"
  "chmod 777"
)

for pattern in "${blocked_patterns[@]}"; do
  if echo "$command" | grep -qE "$pattern"; then
    echo "Blocked: command matches dangerous pattern '$pattern'" >&2
    exit 2
  fi
done

exit 0

Make it executable and reference it in your settings:

chmod +x .claude/scripts/guard.sh
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": ".claude/scripts/guard.sh"
      }
    ]
  }
}

This approach is easier to maintain, test, and extend. Your team can review and contribute to the script through normal code review.

Desktop Notifications

If you run Claude Code on long tasks, you want to know when it needs your attention. A Notification hook sends a desktop alert:

macOS:

{
  "hooks": {
    "Notification": [
      {
        "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
      }
    ]
  }
}

Linux:

{
  "hooks": {
    "Notification": [
      {
        "command": "notify-send 'Claude Code' 'Awaiting your input'"
      }
    ]
  }
}

You can also use the Stop event to get notified when Claude finishes its response entirely:

{
  "hooks": {
    "Stop": [
      {
        "command": "notify-send 'Claude Code' 'Task completed'"
      }
    ]
  }
}

Session Start: Loading Context Automatically

The SessionStart hook runs when you open a new Claude Code session or resume an existing one. It's useful for injecting context that Claude should be aware of from the beginning.

{
  "hooks": {
    "SessionStart": [
      {
        "command": "echo '{\"message\": \"Current git branch: '$(git branch --show-current)'. Last 3 commits: '$(git log --oneline -3 | tr '\\n' '; ')'\"}'"
      }
    ]
  }
}

This gives Claude immediate awareness of your git state without you having to mention it. The JSON output with a message field gets added to Claude's context.

Practical Session Start Ideas

Here are other things you can inject at session start:

  • Open issues assigned to you: Pull from GitHub or Jira
  • Failed CI status: Check if the current branch has failing tests
  • Environment info: Node version, database status, running services
  • Team conventions: Load relevant rules based on the current directory

Matchers In Depth

The matcher field accepts a regex pattern that's tested against the tool name. Here are common patterns:

MatcherMatches
"Edit"Only the Edit tool
"Edit|Write"Edit or Write tools
"Bash"Only Bash tool calls
"Read"Only file reads
".*"All tools (use carefully)
(omitted)All tools — same as ".*"

Matchers are case-sensitive and use standard regex syntax. The pattern is matched against the full tool name.

Combining Multiple Hooks

You can attach multiple hooks to the same event. They execute in order:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "npx prettier --write \"$TOOL_INPUT_FILE_PATH\""
      },
      {
        "matcher": "Edit|Write",
        "command": "npx eslint --fix \"$TOOL_INPUT_FILE_PATH\""
      }
    ]
  }
}

Both hooks run after every file edit — first Prettier, then ESLint. If the first hook fails (non-zero exit), the second still runs.

Async Hooks

By default, hooks block Claude's execution until they complete. For tasks that don't need to finish before Claude continues, add "async": true:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "curl -X POST https://your-logging-service.com/api/log -d '{\"event\": \"file_edited\"}'",
        "async": true
      }
    ]
  }
}

Async hooks run in the background. Claude doesn't wait for them and won't see their output. Use them for logging, analytics, or any side effect that doesn't affect Claude's next action.

Practical Hook Recipes

Here are production-ready hooks you can adapt for your projects.

Auto-Lint TypeScript Files Only

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "bash -c 'if [[ \"$TOOL_INPUT_FILE_PATH\" == *.ts || \"$TOOL_INPUT_FILE_PATH\" == *.tsx ]]; then npx eslint --fix \"$TOOL_INPUT_FILE_PATH\"; fi'"
      }
    ]
  }
}

Run Tests After Code Changes

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "bash -c 'if [[ \"$TOOL_INPUT_FILE_PATH\" == *.test.* || \"$TOOL_INPUT_FILE_PATH\" == *.spec.* ]]; then npx jest \"$TOOL_INPUT_FILE_PATH\" --no-coverage; fi'",
        "async": true
      }
    ]
  }
}

This runs tests asynchronously whenever Claude modifies a test file — giving you immediate feedback without slowing Claude down.

Prevent Commits to Main

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "bash -c 'read input; cmd=$(echo \"$input\" | jq -r \".tool_input.command\"); if echo \"$cmd\" | grep -q \"git commit\" && [ \"$(git branch --show-current)\" = \"main\" ]; then echo \"Cannot commit directly to main branch\" >&2; exit 2; fi'"
      }
    ]
  }
}

Log All Tool Calls

{
  "hooks": {
    "PreToolUse": [
      {
        "command": "bash -c 'read input; echo \"$(date -Iseconds) | $input\" >> ~/.claude/tool-log.jsonl'",
        "async": true
      }
    ]
  }
}

This creates an append-only log of every tool Claude calls, useful for auditing or debugging.

Slack Notification on Task Completion

{
  "hooks": {
    "Stop": [
      {
        "command": "curl -s -X POST -H 'Content-type: application/json' --data '{\"text\":\"Claude Code finished a task\"}' $SLACK_WEBHOOK_URL",
        "async": true
      }
    ]
  }
}

Hooks vs. Other Extension Points

Claude Code offers several ways to customize behavior. Here's when to use each:

MechanismDeterministic?Use Case
HooksYes — always runsFormatting, security gates, notifications, logging
CLAUDE.mdNo — model follows instructionsProject context, coding conventions, architecture rules
SkillsNo — model-triggeredReusable workflows, slash commands, specialized tasks
MCP ServersYes — tool availabilityExternal integrations (GitHub, databases, APIs)

The key distinction: hooks guarantee execution. Everything else relies on the model deciding to follow instructions. Use hooks when "probably" isn't good enough — formatting, security, notifications, and compliance checks.

Debugging Hooks

When a hook isn't working as expected:

  1. Check stderr — hook error messages show up in Claude's context. Add echo "debug: something" >&2 to your script to trace execution.

  2. Test the command manually — run your hook command in a terminal with sample JSON piped to stdin:

echo '{"tool_input": {"command": "rm -rf /"}}' | .claude/scripts/guard.sh
echo $?  # Should print 2 (blocked)
  1. Verify the matcher — make sure your regex matches the exact tool name. Tool names are case-sensitive (Bash, not bash).

  2. Check file permissions — scripts need to be executable (chmod +x).

  3. Inspect JSON input — add a logging hook to see exactly what data Claude Code passes:

{
  "hooks": {
    "PreToolUse": [
      {
        "command": "bash -c 'read input; echo \"$input\" >> /tmp/hook-debug.jsonl'",
        "async": true
      }
    ]
  }
}

A Complete Working Configuration

Here's a full .claude/settings.json that combines the most useful hooks into a single configuration:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": ".claude/scripts/guard.sh"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "npx prettier --write \"$TOOL_INPUT_FILE_PATH\""
      }
    ],
    "Notification": [
      {
        "command": "notify-send 'Claude Code' 'Awaiting your input'"
      }
    ],
    "Stop": [
      {
        "command": "notify-send 'Claude Code' 'Task completed'"
      }
    ]
  }
}

This gives you:

  • Security: dangerous commands blocked before execution
  • Quality: every file formatted automatically after edits
  • Awareness: desktop notifications when Claude needs you or finishes

Start with this configuration and add hooks as your workflow demands. The beauty of hooks is that they compound — each one removes a manual step from your process, and they never forget to run.

Key Takeaways

  • Hooks are shell commands that run at specific lifecycle events in Claude Code. They're deterministic — they always execute, unlike prompt-based instructions.
  • PreToolUse is the most powerful event. It can block dangerous actions before they happen by exiting with code 2.
  • PostToolUse is the most common event. Use it for auto-formatting, linting, and post-edit validation.
  • Configure hooks in .claude/settings.json (project-wide, shareable) or ~/.claude/settings.json (global, personal).
  • Use matchers to narrow which tools trigger a hook. "Edit|Write" for file changes, "Bash" for commands.
  • Start simple — auto-format and notifications cover most needs. Add security gates and logging as your team grows.
  • Use "async": true for hooks that don't need to finish before Claude continues (logging, notifications, analytics).
  • For complex logic, use external scripts instead of inline commands. They're easier to test, review, and maintain.

Ship 10x faster with Claude Code

Production-ready CLAUDE.md templates, MCP server configs, custom hooks, and battle-tested workflows. Stop configuring, start building.

  • CLAUDE.md templates for 6+ frameworks with MCP server configs
  • 8+ custom hooks: Pre-commit, lint, test, format & more ready to go
  • Prompt library: 50+ curated prompts and workflow templates