🔥 Launch tonight — Claude Code Power Prompts PDF 50p (just 50p tonight)30 battle-tested prompts · 8-page PDF · paste into CLAUDE.md · flat 50p tonight

Claude Code Hooks

Run shell commands automatically at lifecycle events — before tool calls, after edits, when Claude stops, or on any notification. Hooks turn Claude into a self-correcting autonomous agent.

What Are Hooks?

Claude Code hooks are shell commands wired to lifecycle events in your .claude/settings.json. When a matching event fires, Claude runs your command synchronously and reads the output — so if your test suite fails after an edit, Claude sees the failure and fixes it automatically.

PreToolUse
Fires before a tool executes. Return non-zero to block the tool call.
PostToolUse
Fires after a tool finishes. Output is fed back to Claude automatically.
Stop
Fires when Claude finishes its response turn.
Notification
Fires when Claude sends a notification or alert.

Basic Structure

Hooks live in .claude/settings.json at your project root (or in ~/.claude/settings.json for global hooks). Each event takes an array of matcher/hooks pairs:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",   // regex matched against tool name
        "hooks": [
          {
            "type": "command",
            "command": "npm test -- --passWithNoTests 2>&1 | tail -20"
          }
        ]
      }
    ]
  }
}

The matcher is a regex tested against the tool name (e.g., Bash, Write, Edit, Read, WebSearch). Use ".*" to match all tools.

Hook Recipes

Auto-run tests after every file write
Claude writes code → tests run → Claude fixes failures automatically
PostToolUse Testing
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": "npm test 2>&1 | tail -30" }]
      }
    ]
  }
}

With this hook, Claude enters a write→test→fix loop automatically. You don't need to say "run the tests" — it just happens.

Run linter after edits
Keep code style consistent without manual ESLint runs
PostToolUse Linting
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": "npx eslint --fix $(git diff --name-only HEAD | grep '\\.js$' | tr '\n' ' ') 2>&1" }]
      }
    ]
  }
}
Type-check TypeScript after edits
Catch type errors immediately — Claude sees them and self-corrects
PostToolUse TypeScript
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": "npx tsc --noEmit 2>&1 | head -40" }]
      }
    ]
  }
}
Desktop notification when Claude finishes
Get notified when a long-running task completes — step away without missing it
Stop Productivity
# macOS
{
  "hooks": {
    "Stop": [
      {
        "matcher": ".*",
        "hooks": [{ "type": "command", "command": "osascript -e 'display notification \"Claude Code finished\" with title \"Claude Code\"'" }]
      }
    ]
  }
}

# Linux (notify-send)
{
  "hooks": {
    "Stop": [
      {
        "matcher": ".*",
        "hooks": [{ "type": "command", "command": "notify-send 'Claude Code' 'Task finished'" }]
      }
    ]
  }
}
Block dangerous shell commands (safety guard)
Pre-approve or block certain Bash commands before Claude runs them
PreToolUse Safety
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'echo \"$CLAUDE_TOOL_INPUT\" | python3 -c \"\nimport sys, json, os\ninput = json.load(sys.stdin)\ncmd = input.get(\\\"command\\\", \\\"\\\")\nif any(bad in cmd for bad in [\\\"rm -rf /\\\", \\\"DROP TABLE\\\", \\\"format c:\\\"]):\n    print(\\\"BLOCKED: dangerous command detected\\\", file=sys.stderr)\n    sys.exit(1)\n\"'"
          }
        ]
      }
    ]
  }
}

When a PreToolUse hook exits with non-zero, Claude is told the tool was blocked and the reason. A simpler approach: use permission mode settings in CLAUDE.md.

Log every tool call to a file
Build an audit trail of what Claude does during a session
PostToolUse Auditing
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"[$(date -u +%FT%TZ)] $CLAUDE_TOOL_NAME\" >> .claude/audit.log"
          }
        ]
      }
    ]
  }
}
Auto-format Python with Black after edits
Consistent formatting without ever mentioning style
PostToolUse Python
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "git diff --name-only HEAD | grep '\\.py$' | xargs black 2>&1"
          }
        ]
      }
    ]
  }
}

Environment Variables in Hooks

Claude Code injects these variables into hook commands so you can act on context:

# Example: log only Write tool calls with file path
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [{
          "type": "command",
          "command": "python3 -c \"import json,os; inp=json.loads(os.environ['CLAUDE_TOOL_INPUT']); print('[write]', inp.get('file_path','?'))\" >> .claude/writes.log"
        }]
      }
    ]
  }
}

Global vs Project Hooks

Hooks can be defined at two scopes:

Both files are merged — user hooks run alongside project hooks. If a key conflicts, project settings win.

Combining Hooks with CLAUDE.md

Hooks handle when to run; CLAUDE.md handles what Claude should do. The power combination:

Together they create self-enforcing workflows. See the CLAUDE.md guide for how to structure the instructions side.

Frequently Asked Questions

Do hooks block Claude from continuing?

PostToolUse hooks run synchronously — Claude waits for them to finish. If the hook exits with 0, Claude reads the output and continues. If exit code is non-zero in a PreToolUse hook, the tool call is blocked and Claude is notified. Keep hooks fast (<5s) or Claude's response latency will increase noticeably.

Can I use hooks to inject additional context into Claude's conversation?

Yes — anything your hook prints to stdout is fed back as tool output context that Claude reads. This means your hook can emit structured information (JSON, markdown tables) that Claude incorporates into its next response. For example, a PostToolUse hook could run git diff --stat and Claude will see a summary of changed files.

How do I debug a hook that isn't firing?

Run Claude with --debug to see hook evaluation logs. Check: (1) the matcher regex is correct — test it with echo "Write" | grep -P "your-matcher", (2) the settings.json is valid JSON — run cat .claude/settings.json | python3 -m json.tool, (3) the command is executable in a non-interactive shell — test it directly in a clean terminal.

Can hooks access the internet or call APIs?

Yes — hooks run as normal shell commands with your full environment, so they can make HTTP calls (curl), run Python scripts, send webhooks, post to Slack, etc. This is useful for notifications (post to Slack on task completion) or quality gates (call an external linting API).

Are hooks available in Claude Code's VSCode extension?

Yes. The VSCode extension uses the same CLI and reads the same .claude/settings.json. Hooks fire the same way whether you're using the terminal or the IDE panel.

Related Guides