Claude Code Hooks: The Workflow Control Layer That Actually Enforces Your Rules

Abstract geometric shapes representing Anthropic AI technology

You’ve been there. You add a rule to CLAUDE.md — “always run prettier after editing files” — and Claude follows it, most of the time. Then it doesn’t. The formatter doesn’t run, the lint check gets skipped, and you’re back to reviewing diffs manually.

Hooks fix this. Claude Code hooks are shell commands, HTTP endpoints, or LLM prompts that fire deterministically at specific points in Claude’s agentic loop. Unlike CLAUDE.md instructions, which are advisory, hooks are enforced at the execution layer — Claude cannot skip them.

As of early 2026, Claude Code ships with 21 lifecycle events across four hook types. This article covers the two that matter most for daily workflow: PreToolUse and PostToolUse.

How Hooks Work Architecturally

Claude Code’s agent loop is a continuous cycle: receive input → plan → execute tools → observe results → repeat. Hooks intercept this loop at named checkpoints.

Every hook is defined in .claude/settings.json under a hooks key. A hook entry has three parts: the lifecycle event name, an optional matcher (a regex against tool names), and the handler definition — either a shell command, an HTTP endpoint, or an LLM prompt.

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

That’s it. Every file Claude writes or edits now auto-formats. No CLAUDE.md reminders, no hoping Claude remembers — the formatter runs on every single Write or Edit tool call, period.

PreToolUse: Enforce Before Claude Acts

PreToolUse fires before Claude executes any tool. Your hook receives the full tool call — name, inputs, arguments — and can return one of three signals:

  • Exit 0 → allow the tool call to proceed
  • Exit 2 → block the tool call; Claude receives your error message and adjusts
  • Exit 1 → hook error; Claude proceeds but logs the failure

This makes PreToolUse the right place for guardrails. Here’s a real example: blocking npm in a bun project.

#!/bin/bash
# .claude/hooks/check-package-manager.sh
# Blocks npm commands in projects that use bun

if echo "$CLAUDE_TOOL_INPUT_COMMAND" | grep -qE "^npm "; then
  echo "Error: This project uses bun, not npm. Use: bun install / bun run / bun add" >&2
  exit 2
fi
exit 0

Wire it in settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/check-package-manager.sh"
          }
        ]
      }
    ]
  }
}

Now when Claude tries npm install, the hook exits 2, Claude sees the error message, and it switches to bun install without you intervening. The correction happens in the same turn.

Another production pattern: blocking writes to protected paths.

#!/bin/bash
# Prevent Claude from modifying migration files already run in production
if echo "$CLAUDE_TOOL_INPUT_FILE_PATH" | grep -qE "db/migrations/"; then
  echo "Error: Migration files are immutable after deployment. Create a new migration instead." >&2
  exit 2
fi
exit 0

PostToolUse: React After Claude Acts

PostToolUse fires after a tool completes successfully. It can’t block execution, but it can provide feedback — and it can run any side-effect you need automatically.

Auto-format every edit:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx prettier --write "$CLAUDE_TOOL_INPUT_FILE_PATH" 2>/dev/null || true"
          }
        ]
      }
    ]
  }
}

Run tests after code changes:

#!/bin/bash
# Run affected tests after any source file edit
FILE="$CLAUDE_TOOL_INPUT_FILE_PATH"
if echo "$FILE" | grep -qE "\.(ts|js|py)$"; then
  if [ -f "package.json" ]; then
    npx jest --testPathPattern="$(basename ${FILE%.*})" --passWithNoTests 2>&1 | tail -5
  fi
fi

Desktop notification on task completion:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification "Claude finished" with title "Claude Code"'"
          }
        ]
      }
    ]
  }
}

Environment Variables Available to Hooks

Claude Code exposes context about the triggering tool call through environment variables. The ones you’ll use most:

VariableValue
$CLAUDE_TOOL_NAMEName of the tool being called (e.g., Edit, Bash, Write)
$CLAUDE_TOOL_INPUT_FILE_PATHFile path for Edit, Write, Read calls
$CLAUDE_TOOL_INPUT_COMMANDShell command for Bash calls
$CLAUDE_SESSION_IDCurrent session ID — useful for audit logging
$CLAUDE_TOOL_RESULT_OUTPUTOutput of the tool (PostToolUse only)

These are injected by Claude Code before your hook runs. You don’t configure them — they’re always there.

The Model Question: Which Claude Runs Agentic Tasks?

One practical consideration for hook-heavy workflows: the default model affects how well Claude responds to hook feedback. As of May 2026:

  • claude-opus-4-7 ($5/MTok input, $25/MTok output) — highest agentic coding capability; best at interpreting hook rejection messages and self-correcting without re-asking
  • claude-sonnet-4-6 ($3/MTok input, $15/MTok output) — strong balance of speed and reasoning; handles most hook-corrected flows well
  • claude-haiku-4-5-20251001 ($1/MTok input, $5/MTok output) — fastest; may require more explicit hook messages to course-correct reliably

For workflows with complex PreToolUse guardrails — especially ones that provide long error messages with corrective instructions — Opus 4.7 handles the feedback loop most reliably. For simpler PostToolUse automation (formatters, notifications), model choice doesn’t matter; the hook runs regardless.

To configure the model: export ANTHROPIC_MODEL=claude-opus-4-7 before launching Claude Code, or set it in your team’s .env.

Hooks vs. CLAUDE.md: When to Use Each

CLAUDE.md is the right place for context, preferences, and guidance — things you want Claude to know about your project. Hooks are the right place for behavior that must happen every time without exception.

The practical test: if failing to follow the instruction costs you five minutes of manual cleanup, put it in a hook. If it’s a style preference or a reminder about architecture decisions, put it in CLAUDE.md. The two are complementary — you’ll likely end up with both in any mature project setup.

A team that gets this right builds CLAUDE.md as documentation for Claude and hooks as the CI/CD equivalent for the agentic loop.

Getting Started

The fastest path to a working hook setup:

  1. Create .claude/settings.json in your project root if it doesn’t exist
  2. Add a PostToolUse hook wired to your formatter — this is low-risk and immediately valuable
  3. Test it by asking Claude to edit a file; the formatter should run automatically
  4. Add PreToolUse guardrails for any tool calls that have caused problems in the past

The official hooks reference is at code.claude.com/docs/en/hooks — it covers all 21 lifecycle events, HTTP handler format, and the full JSON output schema for hook responses.

Hooks are the difference between Claude Code as a powerful suggestion engine and Claude Code as a reliable automation layer. Once you have a PostToolUse formatter running on every edit, going back feels like working without version control.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *