Back to List

The Complete Guide to Claude Code Hooks: Deterministic Automation

2026-03-14·7 min read·AITutorial

Introduction

In my previous CLAUDE.md guide, I mentioned that CLAUDE.md tells Claude "what to do" while settings.json tells Claude "how to do it." And in the advanced guide, I showed a few Hook examples briefly.

But Hooks deserve their own dedicated post. They solve a fundamental problem: CLAUDE.md is advisory — Claude might not follow it. Hooks are deterministic — they always execute.

This post covers Hooks thoroughly.


What Are Hooks

In one sentence: Hooks are Shell commands that auto-execute at specific lifecycle points in Claude Code.

Think of them as the Claude Code equivalent of Git Hooks. Git has pre-commit and post-commit; Claude Code has PreToolUse and PostToolUse.

The key difference is determinism:

CLAUDE.mdHooks
NatureNatural language instructionsShell commands
ExecutionClaude "tries its best" to followSystem auto-executes, 100% guaranteed
On failureClaude might ignore itNon-zero exit code blocks the operation
Best forCoding style, architecture conventionsFormatting, linting, security blocking
AnalogyEmployee handbookAccess control system

Rule of thumb: If violating a rule has serious consequences, use a Hook. If it's "preferably follow but occasional exceptions are fine," use CLAUDE.md.


Event Types

Hooks support the following event types:

EventTriggerTypical Use
PreToolUseBefore a tool callBlock dangerous operations, validate params, permission checks
PostToolUseAfter a tool callAuto-format, lint, logging
NotificationWhen Claude sends a notificationForward to Slack, system notifications
StopWhen Claude finishes a responseAuto-run tests, clean up temp files
SubagentStopWhen a sub-agent completesValidate sub-agent output quality

The most commonly used are PreToolUse and PostToolUse, covering 90% of use cases.


Configuration

Interactive Setup

The simplest approach — type this in Claude Code:

/hooks

This opens an interactive UI that guides you through selecting event types, matchers, and commands. Great for quickly adding a single Hook.

Manual settings.json

More flexible — edit the config file directly:

~/.claude/settings.json              → Global Hooks (all projects)
project-root/.claude/settings.json   → Project-level Hooks (team-shared)

JSON structure:

{
  "hooks": {
    "EventType": [
      {
        "matcher": "matching rule",
        "command": "shell command to execute"
      }
    ]
  }
}

Full example:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "echo 'Checking command safety...'"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "npx prettier --write \"$CLAUDE_FILE_PATH\""
      }
    ]
  }
}

Configuration Hierarchy

Like CLAUDE.md, Hooks have levels too:

~/.claude/settings.json          → Global (personal preferences)
.claude/settings.json            → Project-level (team-shared)

Hooks from both levels are merged and all execute — they don't override each other. Both global and project-level Hooks will fire.


Matcher Rules

The matcher determines which tool calls trigger a Hook.

Matching Methods

MethodSyntaxExample
Exact matchTool name"Bash"
Multi-matchPipe-separated"Edit|Write"
Match allEmpty or omitted"" or omit matcher

Common Tool Names

Tool NameDescription
BashExecute shell commands
EditEdit files (partial modification)
WriteWrite files (full overwrite)
ReadRead files
GlobFile pattern search
GrepContent search
WebFetchHTTP requests
WebSearchWeb search

Matching Tips

// Match all file write operations
"matcher": "Edit|Write"
 
// Only match shell commands
"matcher": "Bash"
 
// Match all tools (omit matcher or leave empty)
"matcher": ""

Hook Input and Output

This is the most powerful part of Hooks — they don't just "run a command." They receive context and can return decisions.

Input (stdin)

Each Hook receives a JSON object via stdin when executed:

{
  "session_id": "abc123",
  "tool_name": "Edit",
  "tool_input": {
    "file_path": "/src/app.tsx",
    "old_string": "...",
    "new_string": "..."
  }
}

You can read this JSON in your Hook command for conditional logic.

Output (exit code)

Exit CodeMeaningEffect
0PassOperation proceeds
2BlockOperation cancelled, stdout shown as reason
Other non-zeroErrorHook itself errored, operation continues (not blocked)

Output (stdout JSON)

Beyond exit codes, Hooks can return JSON via stdout for precise control:

{
  "decision": "block",
  "reason": "Detected rm -rf command, blocked for safety"
}
{
  "decision": "approve"
}

Complete Flow

Claude wants to call a tool
    ↓
System checks PreToolUse Hooks
    ↓
Matching Hook receives stdin JSON
    ↓
Hook executes command, returns exit code + stdout
    ↓
exit 0 → Tool executes
exit 2 → Blocked, reason displayed
    ↓
Tool execution completes
    ↓
System checks PostToolUse Hooks
    ↓
Matching Hook executes (format, lint, etc.)

Practical Examples: General Development

Auto-Format on Save

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

Every time Claude edits or creates a file, Prettier auto-formats it. No more writing "please use Prettier" in CLAUDE.md.

Block Dangerous Commands

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -qE 'rm -rf|git push --force|git reset --hard|drop table|DROP TABLE'; then echo '{\"decision\": \"block\", \"reason\": \"Dangerous operation blocked: destructive command detected\"}'; exit 2; fi"
      }
    ]
  }
}

Far more reliable than writing "don't run dangerous commands" in CLAUDE.md — CLAUDE.md is a suggestion, a Hook is a gate.

Auto-Lint After Edit

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "ext=\"${CLAUDE_FILE_PATH##*.}\"; if [ \"$ext\" = \"ts\" ] || [ \"$ext\" = \"tsx\" ]; then npx eslint --fix \"$CLAUDE_FILE_PATH\" 2>/dev/null; fi"
      }
    ]
  }
}

Runs ESLint auto-fix only on TypeScript files.

Notification on Task Completion

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "command": "osascript -e 'display notification \"$CLAUDE_NOTIFICATION\" with title \"Claude Code\"'"
      }
    ]
  }
}

Shows a macOS system notification. On Windows, use PowerShell's New-BurntToastNotification. On Linux, use notify-send.


Practical Examples: Flutter Development

Flutter projects have their own toolchain, and Hooks integrate perfectly. Here's a complete Flutter development Hooks setup:

Complete Configuration

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -qE 'setState\\('; then echo '{\"decision\": \"block\", \"reason\": \"setState is prohibited. Use Riverpod for state management.\"}'; exit 2; fi"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "if echo \"$CLAUDE_FILE_PATH\" | grep -qE '\\.dart$'; then dart format \"$CLAUDE_FILE_PATH\"; fi"
      },
      {
        "matcher": "Edit|Write",
        "command": "if echo \"$CLAUDE_FILE_PATH\" | grep -qE '\\.dart$'; then dart analyze \"$CLAUDE_FILE_PATH\" 2>&1 | head -20; fi"
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "command": "flutter test --reporter compact 2>&1 | tail -5"
      }
    ]
  }
}

Hook-by-Hook Breakdown

Block setState usage:

{
  "matcher": "Bash",
  "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -qE 'setState\\('; then echo '{\"decision\": \"block\", \"reason\": \"setState is prohibited. Use Riverpod for state management.\"}'; exit 2; fi"
}

If your project standardizes on Riverpod, this Hook blocks setState at the source. Much stronger enforcement than writing "don't use setState" in CLAUDE.md.

Auto-format Dart files:

{
  "matcher": "Edit|Write",
  "command": "if echo \"$CLAUDE_FILE_PATH\" | grep -qE '\\.dart$'; then dart format \"$CLAUDE_FILE_PATH\"; fi"
}

Auto-runs dart format after every .dart file edit, ensuring consistent code style.

Auto-analyze Dart files:

{
  "matcher": "Edit|Write",
  "command": "if echo \"$CLAUDE_FILE_PATH\" | grep -qE '\\.dart$'; then dart analyze \"$CLAUDE_FILE_PATH\" 2>&1 | head -20; fi"
}

Runs dart analyze immediately after edits — catch issues at write time, not build time.

Auto-run tests after response:

{
  "matcher": "",
  "command": "flutter test --reporter compact 2>&1 | tail -5"
}

Automatically runs tests after every Claude response, ensuring changes don't break existing functionality. tail -5 shows just the summary.

Flutter Hooks + CLAUDE.md Together

Hooks and CLAUDE.md work best in combination:

CLAUDE.md                              Hooks
─────────────────────────────          ─────────────────────────────
"Use Riverpod, not setState"         → Block setState (enforced)
"Follow dart format style"           → Auto-run dart format (guaranteed)
"Must pass analyze before commit"    → Auto-run dart analyze (instant feedback)
"All changes must pass tests"        → Auto-run flutter test (continuous verification)

CLAUDE.md tells Claude why to do it. Hooks ensure it actually happens.


Advanced Tips

Hook Types

Hooks in settings.json come in three types:

TypeDescription
commandExecute a shell command (default, most common)
promptInject additional prompt text to Claude
agentLaunch a sub-agent to handle it

command covers most scenarios. prompt type is useful for injecting extra context before specific operations.

Environment Variables

These environment variables are available in Hook commands:

VariableDescriptionAvailable In
$CLAUDE_FILE_PATHPath of the file being operated onPostToolUse (Edit/Write)
$CLAUDE_TOOL_INPUTTool input parameters (JSON)PreToolUse
$CLAUDE_NOTIFICATIONNotification contentNotification
$CLAUDE_SESSION_IDCurrent session IDAll events

Debugging Hooks

When a Hook isn't working, troubleshoot step by step:

  1. Check JSON syntax — A malformed settings.json breaks the entire config
  2. Check matcher — Tool names are case-sensitive: bashBash
  3. Test the command manually — Run the Hook command standalone in your terminal
  4. Check exit codesexit 2 blocks; exit 1 just errors without blocking
  5. Check stderr — Hook stderr output is displayed in Claude Code
# Manually test a Hook command
echo '{"tool_name":"Bash","tool_input":"rm -rf /"}' | your-hook-command
echo $?  # Check exit code

Performance Considerations

  • Hooks execute synchronously and block Claude's operations
  • Avoid time-consuming operations in Hooks (like a full flutter test suite)
  • PostToolUse formatting is typically fast (< 1 second) — safe to use freely
  • Stop events are better for slightly longer operations (like running tests), since Claude has already finished responding

FAQ

Q: How should Hooks and CLAUDE.md work together? A: CLAUDE.md handles "soft rules" (coding style, architecture preferences). Hooks handle "hard rules" (formatting, security blocking). They complement each other — they're not replacements.

Q: What happens when a Hook fails? A: Depends on the exit code. exit 2 blocks the operation. exit 1 or other non-zero values just report an error — the operation continues. This design prevents Hook bugs from disrupting your workflow.

Q: Can a Hook modify Claude's tool input? A: Not directly. But a PreToolUse Hook can block the operation with exit 2, and Claude will adjust its behavior based on the block reason.

Q: What happens when multiple Hooks match the same event? A: They execute in configuration order. If any Hook returns exit 2, the operation is blocked.

Q: Can I use Node.js scripts in Hook commands? A: Absolutely. Hook commands are regular shell commands — you can call any executable:

{
  "command": "node .claude/hooks/check-security.js"
}

Put complex logic in script files and have the Hook command just call them. Much easier to maintain.

Q: What about team members who don't use Claude Code? A: .claude/settings.json only affects Claude Code users. It's completely transparent to everyone else. Safe to commit to Git.


Conclusion

Hooks are the core mechanism for Claude Code automation. Two key takeaways:

  1. CLAUDE.md is advisory, Hooks are guaranteed — Enforce critical rules with Hooks
  2. PreToolUse blocks, PostToolUse fixes — Block dangerous operations before execution, fix code quality after execution

Spending 15 minutes setting up Hooks saves you from manual checks in every Claude Code session.


Further Reading