Claude Code Hooks Explained: What You Can Actually Automate

← Back to Blog

Hooks are the most underrated feature in Claude Code. Most people discover them when they want to do something specific — block a dangerous command, log tool calls, send a notification — and find that the feature handles it cleanly. What they don't realize is that hooks are the foundation for building a real production layer on top of Claude Code.

This is what hooks actually do, how they work, and what's worth building with them.


What hooks do

A Claude Code hook is a shell command (or script) that runs in response to an event in Claude Code's tool execution lifecycle. Before Claude runs a tool, after it runs, before it reads a file — you can intercept these moments and do something: log it, block it, modify the environment, send a notification.

The key thing to understand is that hooks run in your shell, with your permissions, with access to the full tool call context as JSON via stdin. They're not constrained to Claude Code's sandbox. They can do anything your shell can do.

This is what makes them powerful. Your hook can call an external API, write to a database, check a policy file, send a Telegram message, or run a Python script. Claude Code just executes it and waits for the result.


The four hook events

Claude Code exposes four hook points:

PreToolUse. Fires before Claude executes a tool call. The hook receives the tool name and its arguments. If the hook exits with a non-zero code, Claude Code blocks the tool call. This is the enforcement layer — the place where you say "Claude can't do this."

PostToolUse. Fires after a tool call completes. The hook receives the tool name, arguments, and the result. This is the logging and notification layer — you can record what happened, send alerts, update external systems.

Notification. Fires when Claude Code wants to surface a message. Useful for routing notifications to systems other than the terminal — Slack, Telegram, a dashboard.

Stop. Fires when Claude Code's session ends. Useful for cleanup, final logging, session summary.

Most production use cases involve PreToolUse and PostToolUse. The other two are important but narrower.


What you can automate

Security enforcement. PreToolUse is how you build a real security layer. Check whether the file path being written to is in an allowed directory. Check whether the bash command contains patterns you've banned. Block writes to production config files. Reject tool calls that target sensitive paths. This isn't a hint to Claude — it's an enforced boundary that runs before the tool executes, regardless of what Claude decided to do.

Audit logging. PostToolUse lets you build a complete record of what Claude did in a session. Every file write, every bash command, every API call — logged to a file or a database with timestamp, arguments, and result. When something goes wrong in production, you have a log to inspect.

Cost tracking. Every tool call has a cost. PostToolUse lets you track cumulative usage per session, flag when a session is running long, or stop a session that's exceeded a budget. This is especially useful when you're running Claude Code autonomously from a queue runner.

Notifications. When Claude Code finishes a long task, fires a notification hook that sends a Telegram message, updates a dashboard, or pings a webhook. You don't have to watch the terminal.

Context injection. PreToolUse can modify the execution environment before a tool runs. Want Claude Code to have access to the current git branch when it reads a file? Inject it. Want to add a timestamp to every bash command's output? Wrap it in a hook.

Automatic formatting. PostToolUse on file writes can run a linter or formatter automatically, so every file Claude Code touches is clean.


Writing your first hook

Hooks are configured in Claude Code's settings file at ~/.claude/settings.json. A hook entry looks like this:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/check-write-policy.py"
          }
        ]
      }
    ]
  }
}

The hook script receives the full tool call as JSON on stdin. In Python:

import json, sys
payload = json.load(sys.stdin)
tool_name = payload["tool_name"]
tool_input = payload["tool_input"]

# Check the file path
if "/Library/LaunchAgents/" in tool_input.get("path", ""):
    print(json.dumps({"decision": "block", "reason": "LaunchAgents path is restricted"}))
    sys.exit(1)

sys.exit(0)

Exit 0 allows the tool call to proceed. Exit non-zero blocks it. The response body can include a reason that Claude Code surfaces in the session.


Production patterns

Sensitive path enforcement. A PreToolUse hook on Write and Edit that checks the target path against a list of protected directories. Simple to implement, genuinely useful — it catches cases where Claude Code makes a reasonable decision that would have bad consequences in your specific environment.

Session activity logging. A PostToolUse hook that appends a JSONL entry to a session log for every tool call. You end up with a complete machine-readable record of everything Claude did. Pair this with a dashboard that reads the log and you have real-time visibility into active sessions.

Tier-based notifications. Different tool calls warrant different notification levels. A bash command that runs tests: log it. A write to a production config file: Telegram notification immediately. A hook system that routes notifications based on tool type and path pattern gives you the right signal at the right time.


The gotchas

Hooks run synchronously. A slow hook blocks Claude Code. Keep hooks fast. If you need to do something slow (API call, database write), do it async — fire and forget — and don't make Claude Code wait.

Hook failures are noisy. If your hook script has a bug and exits with an error, Claude Code surfaces that as a blocked tool call. Test hooks in isolation before deploying them. A broken hook in a production setup can stop Claude Code from doing anything useful.

The matcher matters. PreToolUse hooks can match on specific tool names. A hook that runs on every single tool call adds latency to everything. Be specific in your matchers.

JSON parsing is your responsibility. Claude Code passes a JSON payload, but it's your script that parses it. Malformed payloads, unexpected fields, tool calls with unusual argument shapes — your hook needs to handle these gracefully or it will block legitimate work.


The hook system is what turns Claude Code from a powerful assistant into a production-grade agent with enforceable policies. Building on top of it — security layers, logging infrastructure, notification routing — is where the real leverage is.


I build with Claude every day and write about what it's actually like to ship AI-powered products. Subscribe at shoofly.dev/newsletter — building AI products in the real world, not what the press releases say.