Skip to content
Lesson 5 of 14

Hooks — Event-Driven Automation

8 min read

What Are Hooks

Hooks are scripts or commands that run automatically in response to specific events during a Claude Code session. When Claude reads a file, writes code, runs a command, or finishes a task, hooks let you intercept those events and execute your own logic -- validating input, formatting output, blocking dangerous operations, or triggering downstream processes.

Think of hooks as event listeners for your AI development workflow. Just like a pre-commit hook in git runs your linter before every commit, a PreToolUse hook in Claude Code can validate tool inputs before Claude takes action. The difference is that hooks cover a much broader range of events and can do far more than just pass/fail checks.

Hook Events

Claude Code exposes a rich set of events you can hook into. The most commonly used ones fall into four categories:

Tool lifecycle events:

  • PreToolUse -- fires before Claude uses any tool (reading a file, writing code, running a command)
  • PostToolUse -- fires after a tool completes, giving you access to the result

Session events:

  • SessionStart -- fires when a new Claude Code session begins
  • SessionEnd -- fires when a session is closed
  • Stop -- fires when Claude finishes responding and is about to return control
  • SubagentStop -- fires when a subagent completes its work

User interaction events:

  • UserPromptSubmit -- fires when the user submits a prompt, before Claude processes it
  • Notification -- fires when Claude sends a notification

Context events:

  • PreCompact -- fires before Claude compacts the conversation, letting you influence what gets preserved

Each event receives relevant context data: for tool events, you get the tool name and its inputs or outputs. For session events, you get session metadata. This context lets your hooks make informed decisions.

Hook Types

There are four types of hooks, each suited to different use cases:

command -- Runs a shell command. The simplest and most common type. Good for running linters, formatters, validators, and scripts.

http -- Sends an HTTP request to a webhook URL. Use this to notify external services, trigger CI pipelines, or log events to monitoring systems.

prompt -- Sends the event context to an AI model for evaluation. The most powerful type -- it lets you create rules that are evaluated by AI rather than simple pattern matching.

agent -- Launches a subagent to handle the event. Use this for complex event handling that requires multi-step reasoning.

Configuration

Hooks are configured in your settings file. You can set them at the global level (~/.claude/settings.json) or the project level (.claude/settings.json). Here is the basic structure:

{
  "hooks": [
    {
      "event": "PreToolUse",
      "type": "command",
      "command": "node .claude/hooks/validate-tool.js",
      "matchers": ["Bash"]
    }
  ]
}

Each hook definition has:

  • event -- which event to listen for
  • type -- command, http, prompt, or agent
  • command (or url, prompt, agent) -- what to execute
  • matchers -- optional array of tool names to filter which tool invocations trigger the hook

Matchers: Filtering Which Tools Trigger Hooks

Matchers let you target specific tools so your hook does not fire on every single tool call. Without matchers, a PreToolUse hook fires every time Claude uses any tool -- which could be dozens of times per session.

{
  "hooks": [
    {
      "event": "PreToolUse",
      "type": "command",
      "command": "node .claude/hooks/block-dangerous.js",
      "matchers": ["Bash"]
    },
    {
      "event": "PostToolUse",
      "type": "command",
      "command": "npx prettier --write",
      "matchers": ["Write"]
    }
  ]
}

In this example, the first hook only fires when Claude is about to run a shell command (Bash tool), and the second hook only fires after Claude writes a file (Write tool).

PreToolUse: Blocking Dangerous Commands

The most popular use case for hooks is preventing Claude from running commands you consider dangerous. Here is a script that blocks destructive operations:

// File: .claude/hooks/block-dangerous.js
// Reads tool input from stdin and blocks dangerous commands

import { readFileSync } from "fs";

const input = JSON.parse(readFileSync("/dev/stdin", "utf-8"));
const command = input.tool_input?.command || "";

const blocked = [
  /rm\s+-rf\s+\//,           // Recursive delete from root
  /git\s+push\s+--force/,    // Force push
  /git\s+reset\s+--hard/,    // Hard reset
  /DROP\s+TABLE/i,           // SQL drop table
  /DROP\s+DATABASE/i,        // SQL drop database
  /:\(\)\{.*\|.*\}/,         // Fork bomb
];

for (const pattern of blocked) {
  if (pattern.test(command)) {
    // Output JSON to block the action
    console.log(JSON.stringify({
      action: "block",
      message: `Blocked dangerous command: ${command}`
    }));
    process.exit(0);
  }
}

// Allow the command to proceed
console.log(JSON.stringify({ action: "allow" }));

When this hook blocks a command, Claude receives the block message and can inform the user or take an alternative approach.

PostToolUse: Auto-Formatting After Writes

After Claude writes a file, you might want to automatically run your formatter to ensure the code matches your project style:

{
  "hooks": [
    {
      "event": "PostToolUse",
      "type": "command",
      "command": "node .claude/hooks/auto-format.js",
      "matchers": ["Write"]
    }
  ]
}
// File: .claude/hooks/auto-format.js
import { readFileSync } from "fs";

const input = JSON.parse(readFileSync("/dev/stdin", "utf-8"));
const filePath = input.tool_input?.file_path || "";

// Only format source code files
const formattable = /\.(ts|tsx|js|jsx|css|json|md)$/;

if (formattable.test(filePath)) {
  // The output tells Claude Code to run the formatter
  console.log(JSON.stringify({
    action: "allow",
    commands: [`npx prettier --write "${filePath}"`]
  }));
} else {
  console.log(JSON.stringify({ action: "allow" }));
}

This ensures every file Claude writes is automatically formatted, eliminating style inconsistencies without any manual intervention.

Stop Hook: Running Checks When Claude Finishes

The Stop hook fires when Claude completes a response. This is the perfect place to run linting, type checking, or test suites to validate Claude's work:

{
  "hooks": [
    {
      "event": "Stop",
      "type": "command",
      "command": "node .claude/hooks/post-check.js"
    }
  ]
}
// File: .claude/hooks/post-check.js
// Run linter after Claude finishes making changes

import { readFileSync } from "fs";

const input = JSON.parse(readFileSync("/dev/stdin", "utf-8"));

// Check if any files were modified in this turn
const filesModified = input.tool_results?.some(
  (r) => r.tool_name === "Write"
);

if (filesModified) {
  console.log(JSON.stringify({
    action: "allow",
    commands: ["npm run lint -- --quiet"]
  }));
} else {
  console.log(JSON.stringify({ action: "allow" }));
}

UserPromptSubmit: Validating Input

The UserPromptSubmit hook lets you inspect and optionally modify user prompts before Claude processes them. This is useful for enforcing conventions or adding automatic context:

{
  "hooks": [
    {
      "event": "UserPromptSubmit",
      "type": "command",
      "command": "node .claude/hooks/enrich-prompt.js"
    }
  ]
}

You could use this to automatically append relevant context, enforce naming conventions in requests, or log prompts for auditing purposes.

Prompt Hooks: AI-Evaluated Rules

Prompt hooks are the most powerful hook type. Instead of writing procedural logic, you write a natural language rule that gets evaluated by an AI model:

{
  "hooks": [
    {
      "event": "PreToolUse",
      "type": "prompt",
      "prompt": "Review this shell command for security risks. Block the command if it could delete data, modify system files, access the network in unexpected ways, or expose secrets. Allow the command if it is a standard development operation like running tests, building, or linting.",
      "matchers": ["Bash"]
    }
  ]
}

The AI evaluates the tool input against your rule and decides whether to allow or block. This is far more flexible than regex patterns -- it can understand context and intent, catching edge cases that procedural rules would miss.

Complete Hook Configuration Example

Here is a full .claude/settings.json with multiple hooks working together:

{
  "hooks": [
    {
      "event": "PreToolUse",
      "type": "command",
      "command": "node .claude/hooks/block-dangerous.js",
      "matchers": ["Bash"]
    },
    {
      "event": "PostToolUse",
      "type": "command",
      "command": "node .claude/hooks/auto-format.js",
      "matchers": ["Write"]
    },
    {
      "event": "Stop",
      "type": "command",
      "command": "node .claude/hooks/run-lint.js"
    },
    {
      "event": "PreToolUse",
      "type": "prompt",
      "prompt": "Verify this file write does not overwrite test fixtures, migration files, or lock files without explicit user intent.",
      "matchers": ["Write"]
    }
  ]
}

This configuration blocks dangerous shell commands, auto-formats written files, runs the linter after each response, and uses AI to evaluate file writes against a policy. Together, these hooks create a comprehensive safety net around Claude's actions.

Hook Responses

Hooks communicate back to Claude Code through JSON output:

  • {"action": "allow"} -- let the operation proceed
  • {"action": "block", "message": "reason"} -- stop the operation and show the message
  • {"action": "allow", "commands": ["cmd1", "cmd2"]} -- allow and run follow-up commands

For PreToolUse hooks, you can also modify the tool input before it executes, giving you the ability to sanitize or transform Claude's intended operations.

Best Practices

Keep hooks fast. Every hook adds latency to Claude's workflow. Shell command hooks should complete in under a second. If you need complex processing, consider running it asynchronously or moving it to a PostToolUse or Stop hook where latency is less noticeable.

Use matchers aggressively. A hook that fires on every tool call adds up quickly. Target only the tools where your hook adds value.

Test hooks thoroughly before deploying them to your team. A buggy PreToolUse hook can block legitimate operations and frustrate users. Start by running your hook script manually with sample input to verify it handles all cases correctly.

Try this exercise: add a PreToolUse hook that blocks any Bash command containing sudo. Add a PostToolUse hook that logs every file write to a local .claude/audit.log file with a timestamp. Run a Claude Code session with these hooks active and verify they work as expected. Then share the configuration with your team by committing the .claude/settings.json and hook scripts to version control.