Claude Code Hooks: Automate Your AI Dev Workflow in 2026
AI & Productivity

Claude Code Hooks: Automate Your AI Dev Workflow in 2026

Ricardo Gil
August 19, 2026
14 min read
#claude #claude-code #ai-tools #developer-productivity #automation
πŸ›’

Products in This Post

Affiliate links

As an Amazon Associate I earn from qualifying purchases at no extra cost to you.

Updated October 21, 2026: checked against the current Claude Code hooks reference. This update fixes how handler types and hook events are described, corrects the JSON input paths in every script, and fixes how exit codes and stderr behave.

If you've been using Claude Code for a while, you probably have a rhythm: open terminal, type claude, describe what you want, review the output. It works. But there's a layer most developers never touch that can make Claude Code feel less like a tool you use and more like an automated teammate running in the background β€” hooks.

Claude Code hooks let you intercept the agent's lifecycle at specific points and inject your own logic. Think of them as middleware for AI-assisted development. They're defined in JSON, most commonly run as shell commands, and can do anything from blocking a dangerous operation to firing off a desktop notification when a long-running task finishes. This guide covers what hooks are, how to set them up, and four production-ready examples you can drop into your workflow today.

What Are Claude Code Hooks?

Hooks are user-defined handlers that Claude Code runs at specific points in its execution lifecycle. Each hook has a handler type that decides what actually runs. The official Claude Code hooks reference currently documents five handler types: command (a shell command), http (POST the event JSON to a URL), mcp_tool (call a tool on a configured MCP server), prompt (single-turn evaluation by a Claude model), and agent (a subagent that can use tools before deciding; experimental). This guide uses command hooks throughout.

Separately, each hook is attached to a hook event, the lifecycle moment that triggers it. Claude Code documents many events (SessionStart, UserPromptSubmit, PermissionRequest, SubagentStop, PreCompact, SessionEnd and more). Here are the example hook events this guide focuses on:

  • PreToolUse - fires before Claude executes a tool (Bash, Write, Edit, Read, MCP tools, etc.) and can block it
  • PostToolUse - fires after a tool call succeeds
  • Notification - fires when Claude Code sends a notification, for example when it needs a permission decision or has been waiting for your input
  • Stop - fires when Claude finishes responding

A command hook signals back through its exit code. Exit 0 means no objection, and the action proceeds. Exit 2 is a blocking error: from a PreToolUse hook it blocks the tool call, and your stderr is sent to Claude as the reason, so you can literally tell Claude 'don't do that, here's why' and it will adapt. Any other exit code is a non-blocking error: the action proceeds, and you see a short notice in the transcript.

Hooks can live in several places: ~/.claude/settings.json (all your projects), .claude/settings.json in a repo (shared with the team), .claude/settings.local.json (just you, gitignored), plus managed policy settings, plugins, and skill or subagent frontmatter. Hooks from these files merge rather than override each other: a global safety gate and a project-specific linter both run. If the same handler is defined in more than one file, it runs once. Type /hooks inside Claude Code to browse every configured hook and see which file it came from.

Setting Up Your First Hook

Open or create ~/.claude/settings.json and add a hooks key:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/pre-bash.sh"
          }
        ]
      }
    ]
  }
}

The matcher field filters by tool name. A plain name like Bash or Write is an exact match, and Edit|Write matches either tool. *, an empty string, or leaving the matcher out matches every tool. Any other characters make it a JavaScript regex, so mcp__github__.* matches every tool from a GitHub MCP server (MCP tools are named mcp__<server>__<tool>). Some events, like Stop, ignore matchers and always fire.

The hook receives the event as JSON on stdin. Along with common fields like session_id, cwd and hook_event_name, a tool event includes tool_name and tool_input, the arguments Claude passed to the tool. For a Bash call it looks like this:

json
{
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "npm test", "description": "Run test suite" },
  "cwd": "/home/you/project",
  "session_id": "..."
}

Here's the minimal scaffold for a PreToolUse hook script:

bash
#!/usr/bin/env bash
# ~/.claude/hooks/pre-bash.sh

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

echo "Hook received: $CMD" >&2

exit 0

Make it executable with chmod +x ~/.claude/hooks/pre-bash.sh and you're live. (With exit 0, the stderr line goes to the debug log, not to Claude. More on that under Debugging.)

Hook Example 1: Block Dangerous Commands

The most immediately useful hook is a safety gate that prevents Claude from accidentally running destructive commands. This is especially important when you're iterating fast and Claude has broad filesystem access.

bash
#!/usr/bin/env bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

DANGER_PATTERNS=("rm -rf /" "DROP TABLE" "truncate.*production" "> /dev/sda" "mkfs\.")

for pattern in "${DANGER_PATTERNS[@]}"; do
  if echo "$CMD" | grep -qiE "$pattern"; then
    echo "BLOCKED: matches danger pattern '$pattern'" >&2
    exit 2
  fi
done

exit 0

When Claude tries to run something that matches, the call is blocked and Claude sees your stderr message, so it can explain why it can't proceed or suggest an alternative. This is much more useful than a blunt permission denial - Claude gets context and can adapt. (The rm -rf / pattern also catches rm -rf /tmp/..., which is a deliberate over-block.)

Two caveats. First, pattern lists are a guardrail, not a security boundary: a determined command can always be phrased to slip past a regex. Second, a safety gate fails open: if the script crashes (for example, jq isn't installed), it exits with a code other than 2 and the command runs anyway. For structured control, a PreToolUse hook can instead exit 0 and print JSON with hookSpecificOutput.permissionDecision set to "deny" (or "ask") and a permissionDecisionReason.

Hook Example 2: Auto-Lint After File Edits

A PostToolUse hook on Write or Edit lets you enforce code quality automatically. Every time Claude writes a file, run ESLint, Prettier, or whatever your project uses:

bash
#!/usr/bin/env bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ "$FILE_PATH" =~ \.(js|ts|jsx|tsx)$ ]]; then
  cd "${CLAUDE_PROJECT_DIR:-$(dirname "$FILE_PATH")}" || exit 0
  if ! OUTPUT=$(npx eslint --fix "$FILE_PATH" 2>&1); then
    echo "ESLint found issues in $FILE_PATH that could not be auto-fixed:" >&2
    echo "$OUTPUT" >&2
    exit 2
  fi
fi

exit 0

Wire it up in settings.json. One matcher entry covers both tools:

json
{
  "hooks": {
    "PostToolUse": [
      { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "~/.claude/hooks/post-write.sh" }] }
    ]
  }
}

Now every file Claude edits gets linted immediately. The exit 2 matters: from a PostToolUse hook, exit 2 sends your stderr to Claude (the edit has already happened), so unfixable lint errors become feedback it acts on right away. With exit 0, Claude never sees the stderr. CLAUDE_PROJECT_DIR is set by Claude Code to the project root, so npx finds your local ESLint and config. Note that Claude can also change files by running shell commands, which this hook won't see. If you need every change covered, the docs suggest also adding a Stop hook that scans the working tree once per turn.

Hook Example 3: Command Audit Log

When you're running Claude Code in a shared dev environment or on production-adjacent infrastructure, you want an audit trail. A simple hook on the Bash matcher can log every command Claude tries to run, with a timestamp:

bash
#!/usr/bin/env bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
LOG_FILE="$HOME/.claude/audit.log"

echo "$(date -u +"%Y-%m-%dT%H:%M:%SZ") CMD: $CMD" >> "$LOG_FILE"

exit 0

Register it under PreToolUse to log every attempt, including ones your safety gate blocks, or under PostToolUse to log only commands that succeeded. If you use Claude Code natively on Windows, match Bash|PowerShell so PowerShell tool calls are logged too. Drop this in ~/.claude/settings.json globally so it covers every Claude session. If you're running a home lab server - something like a Beelink EQ12 Mini PC (~$189) running Proxmox or lightweight Linux - you can ship this log to Loki, Datadog, or any aggregator over your local network. The EQ12's N100 processor handles Claude Code sessions with room to spare, and its low idle power draw makes it a solid always-on dev node.

Hook Example 4: Desktop Notification on Task Complete

Long-running Claude Code tasks can take minutes. The Stop hook fires when Claude finishes responding, which makes it perfect for a system notification so you can step away and come back when it's done:

bash
#!/usr/bin/env bash
cat > /dev/null   # consume the JSON input; we don't need it here

# macOS
if command -v osascript &>/dev/null; then
  osascript -e 'display notification "Claude finished responding" with title "Claude Code" sound name "Glass"'
fi

# Linux (needs a desktop notification daemon)
if command -v notify-send &>/dev/null; then
  notify-send "Claude Code" "Claude finished responding"
fi

# Windows (WSL) - run in the background so the dialog doesn't hold the hook open
if command -v powershell.exe &>/dev/null; then
  powershell.exe -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('Claude finished responding', 'Claude Code')" &>/dev/null &
fi

exit 0

Wire it up under the Stop event in your global settings (Stop doesn't use a matcher):

json
{
  "hooks": {
    "Stop": [
      { "hooks": [{ "type": "command", "command": "~/.claude/hooks/on-stop.sh" }] }
    ]
  }
}

It's zero overhead during normal operation and immediately useful the first time you kick off a large refactor and want to focus on something else. Want an alert when Claude is waiting on you (a permission prompt, or idle for your input) rather than finished? Register the same script under the Notification event too. On macOS, osascript notifications go through Script Editor; if Script Editor doesn't have notification permission, the command fails silently, so test it once from your terminal. Don't make a Stop hook exit 2 unless you mean it: that tells Claude to keep working instead of stopping.

Organizing Hooks at Scale

As your hook library grows, structure matters. A layout that scales well:

bash
~/.claude/
+-- settings.json          # Global hook registration
+-- hooks/
    +-- pre-bash.sh        # Safety gate
    +-- post-write.sh      # Auto-lint
    +-- audit-log.sh       # Audit trail
    +-- on-stop.sh         # Notifications

For teams, commit a .claude/settings.json at the repo root with project-specific hooks, and reference scripts in the repo with "$CLAUDE_PROJECT_DIR"/.claude/hooks/... so the paths work on everyone's machine. Put personal tweaks in .claude/settings.local.json. Keep hooks with credentials or external API calls in the global ~/.claude/ directory and out of version control.

If you're building a serious home lab AI stack - multiple Claude sessions, Ollama serving local models, n8n orchestrating workflows - you'll want solid hardware. Adding 32GB of DDR4 RAM (~$59) prevents memory pressure when running multiple models and dev tools simultaneously. For storing model weights locally, a WD Black 2TB NVMe SSD (~$129) gives you the throughput to load 7B-13B parameter models in seconds rather than minutes.

Debugging Hooks

Hooks that fail silently are the worst. It helps to know exactly where your output goes:

  • Exit 0: Claude never sees stderr. Stdout is parsed as JSON if it's a single JSON object; otherwise, for most events, it only goes to the debug log.
  • Exit 2: stderr becomes the blocking message. For PreToolUse, PostToolUse and Stop, Claude receives it as feedback.
  • Any other code: the transcript shows a non-blocking error notice with the first line of stderr. Claude doesn't see it.

To see full hook output, exit codes, and stderr, start Claude Code with claude --debug (or claude --debug-file /tmp/claude.log and tail -f it), or run /debug mid-session.

Test hooks independently before wiring them into Claude, using the same JSON shape Claude sends:

bash
# Simulate what Claude sends to a PreToolUse Bash hook
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"ls -la /tmp"}}' | ~/.claude/hooks/pre-bash.sh
echo "Exit code: $?"

set -euo pipefail at the top of a script catches uninitialized variables and pipeline failures, but remember what a crash means: exit 1 is a non-blocking error, so Claude proceeds anyway. That's fine for a logger, and dangerous for a safety gate. Blocking requires exit 2, or exit 0 with a JSON decision (permissionDecision: "deny" for PreToolUse, "decision": "block" for events like PostToolUse and Stop). One more common gotcha: if your shell profile prints something (an unconditional echo in ~/.bashrc), that text can land in front of your JSON output and Claude Code will silently ignore the JSON.

Hooks vs. Skills (Custom Slash Commands)

It's worth clarifying when to reach for hooks vs. custom slash commands. Claude Code has merged custom commands into skills: a file at .claude/commands/deploy.md still works, but .claude/skills/deploy/SKILL.md is the recommended format, and both give you /deploy. Skills are instructions Claude follows when you type the command (or when Claude decides they're relevant). Hooks are deterministic and fire automatically, with no user action and no reliance on Claude choosing to run them.

Use hooks for enforcement (lint, safety gates, audit), observability (logging, notifications), and automatic side effects (formatting, tagging). Use skills for explicit workflows you initiate, multi-step procedures that need human checkpoints, and anything that benefits from Claude's reasoning before acting.

The two complement each other well. A /deploy skill can drive a deployment, and a PostToolUse hook on Bash can automatically log every command that deployment runs.

Going Further with Claude Code Hooks

Once the basics are in place, the hooks system goes a lot further than shell scripts:

Multiple hooks per event - you can register several handlers for the same event: a safety gate, a logger, and a notifier, each a separate script that does one thing well. Note that matching hooks run in parallel, not as a chain, so don't write one hook that depends on another having run first.

MCP tools - Model Context Protocol tools go through PreToolUse and PostToolUse just like built-in tools, so a matcher like mcp__mydb__.* lets you gate and log calls to a custom MCP server (database queries, internal APIs, IoT sensors). The mcp_tool handler type also works in the other direction: a hook can call a tool on one of your configured MCP servers.

HTTP hooks - you no longer need a script that shells out to curl. The http handler type POSTs the event JSON straight to a URL. If you're running n8n or a home lab webhook server, you can route hook events there and trigger arbitrarily complex automations: update a database, send a Slack message, or kick off a CI pipeline whenever Claude completes a specific type of task.

Prompt and agent hooks - for checks that need judgment rather than a regex, a prompt hook asks a Claude model to evaluate the event and return a decision, and an agent hook (experimental) spawns a subagent that can read files before deciding.

If you're doing serious Claude Code work at your desk, ergonomics matter. The Logitech MX Keys keyboard (~$99) is genuinely worth it for long coding sessions - the key travel and tactile feedback reduce fatigue, and USB-C charging means one less proprietary cable on your desk.

Getting Started Today

Hooks have the best ROI of any Claude Code feature I've used. The setup is low - a few JSON lines and a shell script - but the payoff compounds every session. Start with the audit log (always useful), add the safety gate next, and build from there based on your workflow's actual pain points.

The fact that hooks can talk back to Claude (exit-2 stderr or a JSON decision) is what makes them genuinely powerful. You're not just filtering - you're giving Claude more context to make better decisions. That's the direction this whole ecosystem is moving: less Claude doing things blindly, more Claude operating within guardrails you define and understanding the constraints you've set.

For full event schemas and every input field, keep the hooks reference and the hooks guide bookmarked. They change often. The best hooks tend to be project-specific - shaped by the actual friction points in a real codebase - which is exactly why this extensibility mechanism exists.

Want Claude dialed in for your workflow?

Book a 1-on-1 Claude setup call: 60 minutes, live, in English or Spanish, recorded for you. $127. Book the call β†’

Or start with the Claude Prompt Pack: 50 bilingual prompts, $17, instant download.

πŸ“¬Weekly Newsletter

Get the best home lab & AI content

No spam. One email per week. Unsubscribe anytime.

Share this article