Claude Code Hooks: Complete Tutorial

Claude Code Hooks let you block dangerous AI agent commands, log every action, and get notifications when tasks complete. Five hook types give you programmable control over your agent's workflow—turning AI coding from automated chaos into supervised precision.

Claude Code Hooks: Complete Tutorial

💡 TL;DR - The 30 Seconds Version

🛡️ Claude Code Hooks add 5 programmable checkpoints that let you block dangerous commands, log every action, and control your AI agent's workflow.

⚡ Each hook adds only 50-200ms delay but can prevent disasters like accidental file deletion with rm -rf commands.

🔧 Setup requires adding a hooks section to settings.json and creating Python scripts that read from stdin and write to stdout.

📊 Pre-tool-use hooks run before commands execute, while post-tool-use hooks log actions after completion for full observability.

🚀 This turns AI agents from black boxes into supervised tools, solving the three big problems: safety, observability, and control.

Claude Code Hooks let you control and monitor your AI agent at key points during its workflow. You can block dangerous commands, log all actions, and get notified when tasks complete.

What Are Claude Code Hooks?

Think of hooks as checkpoints in your agent's process. Every time Claude Code wants to run a command, read a file, or finish a task, it hits one of these checkpoints. You can write custom scripts that run at each checkpoint to:

  • Block commands you don't want to run
  • Log everything the agent does
  • Get notifications when tasks complete
  • Analyze the agent's behavior

The Five Hook Types

Claude Code offers five hooks that trigger at different times:

1. Pre-Tool Use

Runs before any tool executes. This is your safety net - you can block any command here.

Example use: Stop the agent from deleting files with rm -rf

2. Post-Tool Use

Runs after a tool completes. Perfect for logging what just happened.

Example use: Record every file the agent created or modified

3. Notification

Runs when Claude Code needs your input or permission.

Example use: Play a sound when the agent asks for approval

4. Stop

Runs when Claude Code finishes responding to you.

Example use: Save the entire conversation to a file

5. Sub-Agent Stop

Runs when a parallel sub-agent completes its task.

Example use: Get notified when background processes finish

Setting Up Hooks

Step 1: Configure Settings.json

Open your Claude Code settings and add a hooks section:

{
  "hooks": {
    "pre-tool-use": [
      {
        "matcher": {},
        "commands": ["uv run hooks/pre-tool-use.py"]
      }
    ],
    "post-tool-use": [
      {
        "matcher": {},
        "commands": ["uv run hooks/post-tool-use.py"]
      }
    ],
    "notification": [
      {
        "matcher": {},
        "commands": ["uv run hooks/notification.py --notify"]
      }
    ],
    "stop": [
      {
        "matcher": {},
        "commands": ["uv run hooks/stop.py --chat"]
      }
    ],
    "sub-agent-stop": [
      {
        "matcher": {},
        "commands": ["uv run hooks/sub-agent-stop.py"]
      }
    ]
  }
}

The empty matcher {} means the hook runs for every event. You can add specific conditions later.

Step 2: Create Hook Scripts

Each hook runs a Python script. Here's the basic structure:

#!/usr/bin/env python3

import sys
import json
from pathlib import Path

def main():
    # Read input from Claude Code
    input_data = json.loads(sys.stdin.read())
    
    # Your logic here
    process_hook(input_data)
    
    # Write output (if needed)
    output = {"status": "success"}
    print(json.dumps(output))

def process_hook(data):
    # Handle the hook event
    pass

if __name__ == "__main__":
    main()

Real Examples

Block Dangerous Commands

Create hooks/pre-tool-use.py:

#!/usr/bin/env python3

import sys
import json
import re

def main():
    input_data = json.loads(sys.stdin.read())
    
    tool_name = input_data.get("tool_name", "")
    tool_input = input_data.get("tool_input", {})
    
    # Block dangerous rm commands
    if tool_name == "bash" and tool_input.get("command"):
        command = tool_input["command"]
        if re.search(r'rm\s+.*-rf', command):
            print(json.dumps({
                "status": "blocked",
                "reason": "Dangerous remove command blocked"
            }))
            return
    
    # Block access to environment files
    if tool_name == "str_replace_editor" and tool_input.get("path"):
        path = tool_input["path"]
        if ".env" in path:
            print(json.dumps({
                "status": "blocked", 
                "reason": "Environment file access blocked"
            }))
            return
    
    # Allow other commands
    print(json.dumps({"status": "allowed"}))

if __name__ == "__main__":
    main()

Log All Actions

Create hooks/post-tool-use.py:

#!/usr/bin/env python3

import sys
import json
from datetime import datetime
from pathlib import Path

def main():
    input_data = json.loads(sys.stdin.read())
    
    # Create log entry
    log_entry = {
        "timestamp": datetime.now().isoformat(),
        "tool_name": input_data.get("tool_name"),
        "tool_input": input_data.get("tool_input"),
        "result": input_data.get("result")
    }
    
    # Save to log file
    log_file = Path("logs/actions.json")
    log_file.parent.mkdir(exist_ok=True)
    
    with open(log_file, "a") as f:
        f.write(json.dumps(log_entry) + "\n")
    
    print(json.dumps({"status": "logged"}))

if __name__ == "__main__":
    main()

Get Voice Notifications

Create hooks/stop.py:

#!/usr/bin/env python3

import sys
import json
import subprocess
import tempfile
from pathlib import Path

def main():
    input_data = json.loads(sys.stdin.read())
    
    # Save full conversation
    if "--chat" in sys.argv:
        save_conversation(input_data)
    
    # Announce completion
    speak("All set and ready for your next step")
    
    print(json.dumps({"status": "complete"}))

def save_conversation(data):
    if "transcript_path" in data:
        # Copy the full chat log
        transcript = Path(data["transcript_path"])
        if transcript.exists():
            backup = Path("logs/chat_backup.json")
            backup.parent.mkdir(exist_ok=True)
            backup.write_text(transcript.read_text())

def speak(text):
    # Use system text-to-speech
    subprocess.run(["say", text], check=False)

if __name__ == "__main__":
    main()

Understanding Hook Data

Each hook receives different data. Here's what you get:

Pre-Tool Use Data

{
  "tool_name": "bash",
  "tool_input": {
    "command": "ls -la"
  }
}

Post-Tool Use Data

{
  "tool_name": "str_replace_editor",
  "tool_input": {
    "command": "view",
    "path": "main.py"
  },
  "result": "File contents here..."
}

Stop Data

{
  "transcript_path": "/path/to/full/conversation.json"
}

Advanced Patterns

Conditional Hooks

Use matchers to run hooks only in specific cases:

{
  "hooks": {
    "pre-tool-use": [
      {
        "matcher": {
          "tool_name": "bash"
        },
        "commands": ["uv run hooks/check-bash.py"]
      }
    ]
  }
}

Multiple Commands

Run several scripts for one hook:

{
  "hooks": {
    "post-tool-use": [
      {
        "matcher": {},
        "commands": [
          "uv run hooks/log-action.py",
          "uv run hooks/notify-slack.py",
          "uv run hooks/update-metrics.py"
        ]
      }
    ]
  }
}

File Organization

Keep your hooks organized:

.claude/
├── settings.json
└── hooks/
    ├── pre-tool-use.py
    ├── post-tool-use.py
    ├── notification.py
    ├── stop.py
    ├── sub-agent-stop.py
    └── utils/
        ├── logging.py
        └── notifications.py

Why This Matters

Hooks solve three big problems with AI agents:

  1. Safety: You can stop dangerous commands before they run
  2. Observability: You can see exactly what your agent does
  3. Control: You can require approval for sensitive operations

As agents become more powerful, these controls become essential. You need to know what your agent is doing and be able to stop it when needed.

Getting Started

  1. Add the hooks section to your settings.json
  2. Create a simple pre-tool-use hook that logs all commands
  3. Test it by running Claude Code and watching the logs
  4. Add more hooks as you need them

Start simple and build up. The power of hooks comes from combining them to create a complete monitoring and control system for your AI agent.

❓ Frequently Asked Questions

Q: Do hooks slow down Claude Code?

A: Yes, but minimally. Each hook adds 50-200ms depending on your script complexity. A simple logging hook adds about 100ms per tool use. For most workflows, this delay is barely noticeable compared to the agent's thinking time.

Q: Can I use languages other than Python for hooks?

A: Yes. You can use any language that reads from stdin and writes to stdout. Popular options include Node.js, shell scripts, and Bun with TypeScript. The examples use Python with Astral UV for single-file execution without dependency management.

Q: What happens if my hook script crashes?

A: Claude Code continues running but skips the failed hook. The error gets logged to your terminal. For pre-tool-use hooks, a crash defaults to allowing the command. Always test your hooks with invalid inputs first.

Q: Where exactly do I put my hook files?

A: Create a hooks directory inside your .claude folder. The full path is `.claude/hooks/your-script.py`. Claude Code runs commands from your project root, so use relative paths in your settings.json configuration.

Q: Can I run multiple pre-tool-use hooks?

A: Yes. List multiple commands in your settings.json array. They run in order, and if any hook blocks the command, execution stops. This lets you separate concerns - one hook for security, another for logging.

Q: How do I debug hooks that aren't working?

A: Add `console.log` statements to your hook scripts and check the terminal output. Test hooks manually by piping sample JSON data: `echo '{"tool_name":"bash"}' | python hooks/pre-tool-use.py`. Check file permissions and paths first.

Q: Do hooks work with Claude Code's programmable mode?

A: Yes. Hooks work in both interactive and programmable modes (`cl -p`). This means you can block dangerous commands and log actions even when running automated scripts or CI/CD pipelines.

Q: Are there security risks with hooks?

A: Hooks run with your user permissions, so they can access anything you can. Never run untrusted hook scripts. Keep hook scripts in version control and review changes carefully. Consider using restrictive file permissions (chmod 700) on sensitive hooks.

Claude Code Beats Cursor: Why Terminal Wins AI Coding War
While competitors built flashy coding IDEs, Anthropic bet on the terminal. Now Claude Code’s command-line approach is winning developers who want AI that integrates with their existing workflows instead of replacing them.
5 Hidden Claude Code Tools That Save 30-50 Hours Monthly
Most engineers use Claude Code like a basic calculator, missing 90% of its capabilities. These five hidden tools can save you 30-50 hours monthly by turning Claude Code from a simple prompt machine into a precision coding instrument.
20 Essential Claude Code Tips for AI-Powered Development
Claude Code transforms coding from struggle to flow, but most developers miss the hidden features that unlock its true power. These 20 essential tips turn your terminal into an AI coding powerhouse that thinks ahead.
How to Build an AI App Without Writing Code: A Beginner’s Guide to Claude Code (Part 1)
Building apps used to require years of coding skills. Now Claude Code lets anyone describe an idea in plain English and watch AI build it in real-time. This changes who can create software, but setup still needs technical steps.
How to Build AI Apps at Scale: An Intermediate Guide to Claude Code (Part 2)
Most AI coding tools autocomplete lines. Claude Code builds entire features, reads git history, and writes pull requests. Anthropic’s terminal-based AI cuts onboarding from weeks to days and works with any IDE or workflow.

Great! You’ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to implicator.ai.

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.