Merlion Technologies

Claude Code Hooks: The Complete Guide to Automating Your AI Coding Workflow

Claude Code Hooks

If you are using Claude Code without hooks, you are leaving a massive chunk of productivity on the table. I spent the last several months building, testing, and breaking Claude Code hooks across multiple production projects, and I can say with confidence that hooks fundamentally changed how I work with AI-assisted development.

Claude Code hooks are user-defined automation scripts that execute at specific lifecycle points during an AI coding session. They let you run shell commands, call HTTP endpoints, fire LLM prompts, or spawn sub-agents automatically — before a tool runs, after a file is edited, when the agent stops, or when a new session starts. The result is deterministic control over a system that is otherwise probabilistic.

This guide covers everything I have learned about claude code hooks: what they are, how to set them up, every hook event available, four handler types, real configuration examples you can copy-paste, performance considerations, and the mistakes I made so you do not have to. Whether you are new to Claude Code or already running it in production, this article will level up your automation game.

Quick Answer

Claude Code hooks are automated triggers defined in your settings.json that fire shell commands, HTTP requests, LLM prompts, or sub-agents at specific lifecycle events like PreToolUse, PostToolUse, Stop, and SessionStart. They give you guaranteed, deterministic enforcement of dev standards — unlike CLAUDE.md rules which are suggestions the model can ignore. Set them up once, and they run every single time without manual intervention.

What Are Claude Code Hooks and Why Do They Matter?

The Core Problem Hooks Solve

Claude Code writes solid code, but it does not always remember to follow your formatting rules, run your test suite, or avoid touching sensitive files. You end up typing the same instructions over and over — “run prettier,” “do not modify .env,” “execute tests before finishing.” Hooks eliminate this entirely.

I learned this the hard way. On one project, Claude Code reformatted files I explicitly asked it not to touch. On another, it decided it was “done” when half the acceptance criteria were unmet. Hooks fixed both problems permanently. A PreToolUse hook now blocks writes to protected files every time. A Stop hook forces verification before the agent can finish. No more babysitting.

Hooks vs. CLAUDE.md vs. MCP: When to Use What

This is the distinction most articles miss. CLAUDE.md instructions are suggestions — the model usually follows them, but it is not guaranteed. Hooks are deterministic rules that always execute. MCP servers extend Claude Code with external tools and data sources.

Feature

CLAUDE.md

Hooks

MCP Servers

Execution

Probabilistic (LLM decides)

Deterministic (always runs)

On-demand (tool call)

Best for

Guidelines, preferences, style

Hard rules, automation, blocking

External integrations, data

Enforcement

Can be ignored by model

Cannot be bypassed

Called when model chooses

Setup

Markdown file

JSON + shell scripts

Server config + protocol

Example

“Prefer Bun over npm”

“Always format with Prettier”

“Search Jira tickets”

My rule of thumb: if breaking the rule would cause a real problem (security, formatting, deployment), use a hook. If it is a preference the model can reasonably override, put it in CLAUDE.md. If it needs external data, use MCP.

How Claude Code Hooks Work: Architecture and Lifecycle

The Hook Execution Flow

Every Claude Code session follows a lifecycle: session starts, user submits a prompt, tools execute, agent responds, session ends. Hooks let you inject custom logic at any point in that cycle. The flow looks like this:

Claude Code Event Fires → Matcher Evaluation → If Condition Check → Hook Handler Executes → Result Returned

When an event fires, Claude Code checks all configured hook groups for that event. If a matcher matches (or no matcher is set, meaning match everything), it evaluates the optional “if” condition. If that passes too, the hook handler runs. Your handler receives JSON context on stdin, performs its logic, and communicates results through exit codes and stdout.

Four Hook Handler Types

As of 2026, Claude Code supports four distinct handler types, each suited to different automation patterns:

Handler Type

How It Works

Best For

Command (type: “command”)

Runs a shell command. Receives JSON on stdin, returns exit code + optional JSON on stdout.

Local validation, formatting, file checks, git operations

HTTP (type: “http”)

Sends a POST request with JSON body to a URL endpoint. Response body uses same output format as command hooks.

External service integration, webhooks, remote APIs, team dashboards

Prompt (type: “prompt”)

Sends a prompt to a Claude model for single-turn yes/no evaluation. Returns JSON decision.

Semantic validation, checking if work is complete, nuanced rule enforcement

Agent (type: “agent”)

Spawns a sub-agent with tool access (Read, Grep, Glob). Multi-step reasoning before decision.

Deep verification, running test suites, complex multi-file validation

I use command hooks for 80% of my automation — they are fast, simple, and cover most use cases. I reserve prompt hooks for Stop event verification where I need the model to evaluate whether all tasks were completed. Agent hooks are powerful but slower; I only use them when I need the hook to actually read files or run grep to make a decision.

Every Claude Code Hooks Event Explained

Claude Code currently exposes over 20 lifecycle events. Here are the ones I use most, with the full list in the reference table below.

1. SessionStart

Fires when a session begins, resumes, clears, or compacts. I use this to inject development context — loading git status, recent issues, and environment variables so Claude starts every session with full project awareness. The matcher supports startup, resume, clear, and compact to differentiate start conditions.

2. UserPromptSubmit

Fires when you submit a prompt, before Claude processes it. I use this for two things: validating that prompts include enough context (exit 2 blocks the prompt), and injecting additional context like the current branch name or recent commits. The stdout from this hook is added as context that Claude can see and act on.

3. PreToolUse

This is the most powerful event. It fires before every tool call and can block execution entirely. I have PreToolUse hooks that block writes to .env files, prevent destructive bash commands (rm -rf, DROP TABLE, push –force), and warn about modifications to sensitive files like private keys. The matcher filters by tool name — Bash, Write, Edit, Read — so your hook only fires for relevant operations.

4. PostToolUse

Fires after a tool call succeeds. The classic use case is auto-formatting: every time Claude writes or edits a file, Prettier or Black runs automatically. I also use PostToolUse to auto-stage modified files in git, which saves me from manually adding them later.

5. Stop

Fires when Claude finishes responding. If your hook returns exit code 2, Claude is forced to continue working. This is game-changing for autonomous workflows — combine it with a prompt hook that evaluates task completion, and you get an AI agent that self-verifies its work before stopping.

6. Notification

Fires when Claude needs your attention — permission prompts, idle prompts, auth success. I use this to send desktop notifications on macOS via osascript and Slack messages via webhook URL. No more checking the terminal every 30 seconds during long-running tasks.

7. PreCompact and PostCompact

PreCompact fires before context compaction (when Claude shrinks its context window to save tokens). I use this to back up the current transcript before compaction wipes it. PostCompact fires after, which is useful for re-injecting critical context that may have been lost.

Complete Hook Events Reference

Event

When It Fires

Can Block?

Common Use Case

SessionStart

Session begins/resumes/clears

No

Inject dev context, set env vars

InstructionsLoaded

CLAUDE.md or rules file loaded

No

Monitor config changes

UserPromptSubmit

User submits a prompt

Yes (exit 2)

Validate/enrich prompts

PreToolUse

Before a tool executes

Yes (exit 2)

Block dangerous ops, protect files

PermissionRequest

Permission dialog appears

Yes

Auto-approve/deny specific tools

PermissionDenied

Tool call denied by classifier

No

Retry with {retry: true}

PostToolUse

After tool succeeds

No

Auto-format, git stage, run tests

PostToolUseFailure

After tool fails

No

Error logging, retry logic

Notification

Claude needs attention

No

Desktop/Slack notifications

SubagentStart

Sub-agent spawns

No

Setup for sub-agents

SubagentStop

Sub-agent finishes

No

Cleanup, branch handling

TaskCreated

Task being created

Yes

Validate task parameters

TaskCompleted

Task marked complete

Yes

Verify task output

Stop

Claude finishes responding

Yes (exit 2)

Force task completion verification

StopFailure

Turn ends due to API error

No

Error logging

TeammateIdle

Agent team member goes idle

Yes

Reassign work

PreCompact

Before context compaction

No

Transcript backup

PostCompact

After compaction completes

No

Re-inject critical context

FileChanged

Watched file changes on disk

No

Hot-reload configs

CwdChanged

Working directory changes

No

Environment management (direnv)

WorktreeCreate

Worktree being created

No

Custom git worktree setup

WorktreeRemove

Worktree being removed

No

Cleanup worktree resources

SessionEnd

Session terminates

No

Cleanup, final logging

How to Set Up Claude Code Hooks: Step-by-Step

Configuration File Locations

Hooks are defined in JSON settings files. You have three levels to choose from, and understanding which to use matters for team workflows:

Level

File Path

Scope

When to Use

User-level

~/.claude/settings.json

Applies to all projects for your user

Personal prefs: notifications, global safety blocks

Project-level

.claude/settings.json (in project root)

Shared via git with your team

Team standards: formatting, linting, branch guards

Project-local

.claude/settings.local.json

Project-specific, not committed

Personal project overrides, experimental hooks

I keep dangerous-command blocking and notification hooks at the user level so they apply everywhere. Formatting and test-running hooks go in the project-level file so the entire team benefits. My rule: if it is a personal preference, use user-level. If the team should follow it, commit it to the project.

Your First Hook: Auto-Format on Every File Edit

This is the most common starting point and takes two minutes. Add this to your settings.json:

{

  “hooks”: {

    “PostToolUse”: [

      {

        “matcher”: “Write|Edit|MultiEdit”,

        “hooks”: [

          {

            “type”: “command”,

            “command”: “jq -r ‘.tool_input.file_path’ | xargs npx prettier –write 2>/dev/null; exit 0”

          }

        ]

      }

    ]

  }

}

That is it. Every file Claude writes or edits now gets auto-formatted with Prettier. The matcher “Write|Edit|MultiEdit” ensures the hook only fires for file-modifying tools, not reads or bash commands. The exit 0 at the end ensures the hook never blocks Claude — formatting failures are non-critical.

Blocking Dangerous Commands with PreToolUse

This is the hook I set up on every machine I use. It blocks destructive bash commands before they execute:

#!/bin/bash

# .claude/hooks/block-dangerous.sh

CMD=$(jq -r ‘.tool_input.command’ < /dev/stdin)

for pattern in “rm -rf /” “rm -rf ~” “DROP TABLE” “push.*–force”; do

  if echo “$CMD” | grep -qiE “$pattern”; then

    echo “Blocked: $CMD” >&2

    exit 2

  fi

done

exit 0

Exit code 2 is the key here. It tells Claude Code to block the tool call and feed the stderr message back to Claude as an error. Claude will then attempt an alternative approach. Any other non-zero exit code is a non-blocking error — the operation continues.

Desktop Notifications When Claude Finishes

If you run long tasks and switch to another window, this hook pings you when Claude needs attention. On macOS:

{

  “hooks”: {

    “Notification”: [

      {

        “matcher”: “”,

        “hooks”: [

          {

            “type”: “command”,

            “command”: “osascript -e ‘display notification \”Claude Code needs attention\” with title \”Claude Code\”‘”

          }

        ]

      }

    ]

  }

}

On Linux, replace osascript with notify-send. On Windows, use PowerShell toast notifications. The empty matcher means fire on every notification event.

Matchers, JSON Input/Output, and Exit Codes

How Matchers Work

Matchers are regex strings that filter which actions trigger a hook. They are case-sensitive and match against tool names or event subtypes. Here are the patterns I use daily:

Matcher

What It Matches

Use Case

Bash

Only Bash tool calls

Block dangerous commands

Write|Edit|MultiEdit

Any file-modifying tool

Auto-format after edits

Read|Edit|Write|Bash

Most common tools

Protect sensitive files from all access

mcp__github__.*

All GitHub MCP tools

Log or validate GitHub operations

mcp__.*

All MCP server tools

Audit all external integrations

“” (empty) or .*

Everything

Global logging, universal notifications

Notebook.*

All Notebook tools

Notebook-specific automation

The “if” field adds a second filter layer using a lightweight pattern syntax like Bash(rm *) that matches against tool input. This lets you skip the overhead of spawning a process for hooks that only apply to specific commands.

JSON Input Structure

Every hook receives a JSON payload on stdin with context about the event. Here is what a typical PreToolUse payload for a Bash command looks like:

{

  “session_id”: “abc123”,

  “cwd”: “/your/project”,

  “hook_event_name”: “PreToolUse”,

  “tool_name”: “Bash”,

  “tool_input”: {

    “command”: “npm test”

  }

}

For Write tool calls, tool_input contains file_path and content. For Edit, it includes file_path, old_string, and new_string. You parse these with jq in shell scripts or json.loads() in Python.

Exit Code Reference

The exit code from your hook tells Claude Code what to do next. Getting this wrong is the most common source of hook bugs:

Exit Code

Meaning

Stdout Parsed?

Effect

0

Success

Yes (JSON output)

Action proceeds; stdout shown in verbose mode (Ctrl+O)

2

Blocking error

No (ignored)

Action blocked; stderr fed to Claude as error message

Any other

Non-blocking error

No (ignored)

Action continues; stderr shown in verbose mode

Advanced Claude Code Hooks Patterns I Use in Production

Stop Hook with Self-Verification (Prompt Hook)

This is the pattern that changed my autonomous workflow. Instead of a shell command, this Stop hook uses a prompt handler that asks a Claude model to evaluate whether the task was actually completed:

{

  “hooks”: {

    “Stop”: [

      {

        “hooks”: [

          {

            “type”: “prompt”,

            “prompt”: “Review the conversation. Did the user request get fully completed? Check: all files created, tests passing, no TODO left. Respond with {\”ok\”: true} if done, or {\”ok\”: false, \”reason\”: \”what remains\”} if not.”,

            “timeout”: 30

    }

        ]

      }

    ]

  }

}

If the model returns {“ok”: false}, Claude Code continues working on the unfinished items. This effectively creates a self-verifying agent.

Warning: Stop Hook Infinite Loop

When a Stop hook returns exit 2 (or ok: false for prompt hooks), Claude continues working and will eventually try to stop again — triggering the same hook. If your verification logic never passes, you get an infinite loop. Always check the stop_hook_active field in the JSON input. When it is true, your hook is being called on a subsequent stop attempt, and you should exit 0 to let Claude finish.

Protecting Sensitive Files from All Access

This PreToolUse hook blocks reads, writes, and bash access to any file matching sensitive patterns. I use it to prevent Claude from ever touching credentials, private keys, or env files:

#!/bin/bash

# .claude/hooks/protect-secrets.sh

INPUT=$(cat)

FILE=$(echo “$INPUT” | jq -r ‘.tool_input.file_path // .tool_input.command // “”‘)

if echo “$FILE” | grep -qiE ‘\.env$|\.env\.|\.pem$|\.key$|id_rsa|credentials’; then

  echo “Access to sensitive file blocked” >&2

  exit 2

fi

exit 0

Auto-Commit with GitButler Integration

If you run multiple Claude Code sessions simultaneously, GitButler hooks can automatically isolate each session into its own virtual branch and commit changes per session. The configuration registers PreToolUse, PostToolUse, and Stop hooks that communicate with GitButler CLI:

{

  “hooks”: {

    “PreToolUse”: [{ “matcher”: “Edit|MultiEdit|Write”, “hooks”: [{ “type”: “command”, “command”: “but claude pre-tool” }] }],

    “PostToolUse”: [{ “matcher”: “Edit|MultiEdit|Write”, “hooks”: [{ “type”: “command”, “command”: “but claude post-tool” }] }],

    “Stop”: [{ “matcher”: “”, “hooks”: [{ “type”: “command”, “command”: “but claude stop” }] }]

  }

}

With this setup, three parallel Claude Code sessions will each produce commits on separate branches automatically. No merge conflicts, no manual branch management.

Context Re-Injection After Compaction

Claude Code compacts its context window to save tokens, and sometimes critical instructions from CLAUDE.md get lost in the process. This UserPromptSubmit hook re-injects your core rules on every prompt:

{

  “hooks”: {

    “UserPromptSubmit”: [

      {

        “matcher”: “”,

        “hooks”: [

  {

            “type”: “command”,

            “command”: “cat $CLAUDE_PROJECT_DIR/CLAUDE.md”

          }

        ]

      }

    ]

  }

}

The stdout from UserPromptSubmit hooks is added as context that Claude sees. This means your CLAUDE.md rules are always fresh in context, even after compaction.

Running Tests Asynchronously After File Changes

For hooks that take a long time (like running a full test suite), use the async: true flag so the hook runs in the background without blocking Claude:

{

  “hooks”: {

    “PostToolUse”: [

      {

        “matcher”: “Write|Edit”,

        “hooks”: [

          {

            “type”: “command”,

            “command”: “$CLAUDE_PROJECT_DIR/.claude/hooks/run-tests.sh”,

            “async”: true

  }

        ]

      }

    ]

  }

}

Async hooks fire and forget — they cannot block tool execution or return decisions. Use them for logging, notifications, and background processes where you do not need the result before Claude continues.

Using Claude Code Hooks in CI/CD and Headless Mode

Hooks work identically in headless mode as in interactive mode. This makes them powerful for CI/CD pipelines where Claude Code runs automated tasks. Your PreToolUse safety hooks still block dangerous commands, PostToolUse hooks still log actions, and SessionStart hooks still initialize the environment.

The $CLAUDE_CODE_REMOTE environment variable is set when Claude Code runs in a remote or headless environment. I use it to conditionally skip desktop notifications while keeping Slack webhooks active:

#!/bin/bash

if [ -n “$CLAUDE_CODE_REMOTE” ]; then

  # Headless: send Slack webhook instead

  curl -s -X POST -H “Content-Type: application/json” \

    -d ‘{“text”:”Claude Code task complete”}’ “$SLACK_WEBHOOK_URL”

else

  # Local: desktop notification

osascript -e ‘display notification “Done” with title “Claude Code”‘

fi

Debugging Claude Code Hooks

The /hooks Menu

Type /hooks inside Claude Code to open a read-only browser showing all configured hooks. It lists every event with a count of configured hooks, lets you drill into matchers, and shows the full details of each handler. I use this to verify my configuration is loaded correctly, especially when hooks in project-level vs user-level files conflict.

Verbose Mode (Ctrl+O)

Toggle verbose mode with Ctrl+O inside Claude Code. In verbose mode, stdout and stderr from all hooks are displayed in the terminal. This is essential for debugging — without it, hook output is silent and you have no visibility into what is happening.

Common Debugging Mistakes

Mistake

Symptom

Fix

Reading tool_input from env vars

Hook gets empty data, silently fails

Always read JSON from stdin, not env vars

Forgetting exit 0 on success

Non-zero exit treated as error

Explicitly exit 0 at end of script

Using exit 2 in PostToolUse

Exit 2 has no blocking effect here

Exit 2 only blocks in PreToolUse, UserPromptSubmit, Stop

Hook script not executable

Permission denied errors

Run chmod +x on your hook scripts

Forgetting jq dependency

Parse errors on first run

Install jq: brew install jq / apt install jq

Stop hook infinite loop

Claude never finishes

Check stop_hook_active field, exit 0 if true

Claude Code Hooks vs. Other AI Coding Agent Hooks

Claude Code is not the only tool with hook support in 2026. Here is how the landscape compares:

Feature

Claude Code

Cursor

GitHub Copilot

Handler types

4 (command, HTTP, prompt, agent)

Command only

Command only

Lifecycle events

20+

5 (beforeShellExecution, beforeMCPExecution, beforeReadFile, afterFileEdit, stop)

Limited (preToolUse focus)

Prompt/LLM hooks

Yes (prompt + agent types)

No

No

Async execution

Yes (async: true flag)

No

No

MCP tool matching

Yes (mcp__server__.*)

Yes (beforeMCPExecution)

No

CI/CD headless mode

Full support

No

Limited

Configuration

JSON settings files (3 levels)

JSON

.github/hooks/*.json

The key differentiator for Claude Code is handler diversity. Command hooks handle straightforward checks. Prompt hooks handle semantic evaluation. Agent hooks handle deep analysis. This three-tier system maps naturally to different development requirements, from simple formatting to complex quality gate enforcement.

Frequently Asked Questions About Claude Code Hooks

1. Do Claude Code hooks slow down the coding workflow?

Command hooks add negligible overhead — typically a few milliseconds per invocation. Prompt hooks are slower at 2-8 seconds because they call a Claude model. Agent hooks can take 10-30 seconds for complex verification. Place slow hooks on low-frequency events like Stop (fires once per task) rather than high-frequency events like PreToolUse (fires on every tool call).

2. Can I use hooks without knowing shell scripting?

You need basic shell knowledge for command hooks since they run bash/shell commands. However, you can ask Claude Code itself to write hooks for you — describe what you want in the CLI, and Claude will generate the settings.json configuration and hook scripts. HTTP hooks only need a URL endpoint. Prompt hooks need zero code, just a natural language prompt string.

3. How many hooks can I configure per event?

There is no hard limit. You can register multiple matcher groups under the same event, and each group can have multiple handlers. All matching hooks execute — they do not short-circuit on the first match. However, be mindful of performance: ten synchronous hooks on PreToolUse each taking 500ms means 5 seconds of delay before every tool call.

4. Do hooks work with MCP server tool calls?

Yes. Use matchers like mcp__github__.* to target specific MCP server tools, or mcp__.* to match all MCP tool calls. PreToolUse hooks can block MCP operations, PostToolUse hooks can log them, and you get the same JSON input/output format as with built-in tools.

5. Where should I put hooks — user-level or project-level settings?

User-level (~/.claude/settings.json) for personal preferences that apply everywhere: notifications, global safety blocks, personal formatting. Project-level (.claude/settings.json) for team standards committed to git: shared formatting rules, linting, branch guards, test requirements. Project-local (.claude/settings.local.json) for personal project-specific overrides that should not be committed.

Conclusion

Claude Code hooks transform the AI coding assistant from a tool you have to supervise into an automated development environment that enforces your standards consistently. After months of using them across multiple production projects, I consider hooks non-negotiable for any serious Claude Code workflow.

Start with three hooks — auto-formatting with PostToolUse, dangerous command blocking with PreToolUse, and notifications with the Notification event. That setup takes ten minutes and immediately saves hours of repetitive work. Once you see the value, expand into Stop hooks for self-verification, SessionStart for context injection, and async hooks for background test execution.

The distinction between CLAUDE.md suggestions and hook enforcement is the single most important concept here. CLAUDE.md is a request. A hook is a guarantee. For anything that matters — security, formatting, deployment safety, task verification — use a hook.

Pro Tip

Use the /hooks menu inside Claude Code to verify your configuration is loaded correctly. Combine it with Ctrl+O verbose mode to see real-time hook output. And always test new hooks on a non-critical project first — a misconfigured Stop hook can trap Claude in an infinite loop that is surprisingly hard to break out of.

Picture of Jack Henry

Jack Henry

Jack Henry has a keen interest in software development and a solid understanding of how software products are built. He enjoys learning about coding, system design, and the teamwork behind successful tech projects. Jack brings curiosity, dedication, and fresh thinking to every challenge he takes on.

AI Tools

Staff Augmentation

Custom Software Development

Contact Merlion Technologies