← Back to Blog

Claude Code's Most Underrated Feature: Hooks

Hooks are event-driven triggers that intercept Claude Code at specific points — before it writes a file, after it runs a command, when it needs your input. They let you control Claude's actions, not just react to them.

Yet most engineers skip right past them. Once you start using hooks, you'll wonder how you ever worked without them.

Imagine Claude Code that auto-formats every file it touches. Runs tests after every edit. Pings you on Slack the moment it needs input. Blocks dangerous commands before they execute. Creates git checkpoints before risky operations. Remembers context across sessions. Enforces TDD by refusing to write code until tests exist. That's not a wish list — that's what hooks let you build.

The docs exist, but practical examples are scarce. This post is the guide I wish existed when I started — from understanding the event system to building hooks that actually matter.

TL;DR

I've open-sourced a growing collection of hooks at claude-code-hooks. Tested hooks plus a roadmap of ideas to supercharge your Claude Code workflow.

See It In Action

Here's what hooks look like in practice — blocking dangerous operations before they can execute:

1. Protecting secrets during debugging
Hook blocking .env read
2. Blocking dangerous bash commands
Hook blocking dangerous commands

What Are Hooks, Really?

Think of Claude Code as a loop: it thinks, it acts (reads files, writes code, runs commands), and it repeats. Hooks intercept that loop at specific points — before, during, or after each action.

Claude Code (triggers event) YOUR CODE RUNS HERE Your Hook (any language) validate · modify · notify · act Claude Code (continues) JSON in (stdin) → Your logic → JSON out (stdout)

Your hook receives JSON via stdin (session info, tool name, inputs), does whatever logic you want, and returns a decision. Block the action. Allow it. Modify it. Add context. Send a notification. It's your code, your rules.

There are 13 hook events, way more than most people realize:

Here's how they fit into the lifecycle (from the official docs):

Claude Code Hooks Lifecycle Diagram
Hook lifecycle diagram. Credit: Anthropic

Each hook receives context via stdin as JSON: session ID, tool name, inputs, outputs. You can write hooks in any language.

The challenge? Understanding what data is actually available for each event type takes some digging. So let's make it easier.

Step 1: See What's Happening (The Event Logger)

Before you can build useful hooks, you need to understand what data Claude Code sends for each event. The best way to learn is to log everything.

I built an event-logger for exactly this. It captures every hook event to a daily log file with timestamps, event types, and full payloads. Add it to your .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "*",
      "hooks": [{ "type": "command", "command": "python ~/.claude/hooks/event-logger.py" }]
    }],
    "PostToolUse": [{
      "matcher": "*",
      "hooks": [{ "type": "command", "command": "python ~/.claude/hooks/event-logger.py" }]
    }]
  }
}

This example shows two events, but you can add the logger to any of the 13 events to see their payloads.

Now use Claude Code normally. Then view your logs:

# View today's logs
cat ~/.claude/hooks-logs/$(date +%Y-%m-%d).jsonl | jq

# Filter by event type
cat ~/.claude/hooks-logs/*.jsonl | jq 'select(.hook_event_name=="PreToolUse")'

You'll see exactly what data flows through each event: tool names, file paths, command inputs, everything. This is how you learn what's possible.

Step 2: Python vs Node for Hooks

You can write hooks in any language, but there's an important consideration: startup time.

Hooks run synchronously. Claude Code waits for them to finish before continuing. If your hook takes 500ms to start, every tool call gets 500ms slower. This adds up fast — PreToolUse fires on every single tool call, which can be dozens per session.

Here's the reality:

My recommendation: use Node.js for high-frequency hooks (anything on PreToolUse or PostToolUse). Use Python for exploration, debugging, or hooks that only fire once per session.

The production hooks in my repo are written in Node.js for this reason. The event-logger is in Python since it's only for debugging and runs infrequently.

Step 3: Start Here (High-Value Hooks)

These are the hooks I recommend starting with — immediate value, minimal setup. They're tested and documented in my hooks repo. Once you see how they work, you'll have ideas for dozens more.

1. Block Dangerous Commands

This is the hook I run on every project. It blocks catastrophic bash commands before Claude Code can execute them:

The hook has three safety levels you can configure:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "node ~/.claude/hooks/block-dangerous-commands.js"
      }]
    }]
  }
}

When a dangerous command is blocked, Claude Code sees the reason and adjusts. It's a safety net that costs almost nothing.

2. Protect Secrets

This hook prevents Claude Code from reading, modifying, or exfiltrating sensitive files:

It also blocks bash commands that would expose secrets:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Read|Edit|Write|Bash",
      "hooks": [{
        "type": "command",
        "command": "node ~/.claude/hooks/protect-secrets.js"
      }]
    }]
  }
}

3. Auto-Stage Changes

Every time Claude Code edits or creates a file, this hook automatically runs git add on it:

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{
        "type": "command",
        "command": "node ~/.claude/hooks/auto-stage.js"
      }]
    }]
  }
}

Benefits:

It relies on your .gitignore to exclude sensitive files, so make sure that's set up correctly.

4. Slack Notifications

When Claude Code needs input (permission prompts, idle prompts), get a Slack notification so you don't leave it hanging:

{
  "hooks": {
    "Notification": [{
      "matcher": "permission_prompt|idle_prompt",
      "hooks": [{
        "type": "command",
        "command": "node ~/.claude/hooks/notify-permission.js"
      }]
    }]
  }
}

Understanding the Data Flow

Hooks receive data two ways:

Shell Environment Variables

Stdin JSON Payload

The real data comes via stdin. For PreToolUse:

{
  "session_id": "abc123",
  "cwd": "/path/to/project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf ~/Documents"
  },
  "tool_use_id": "xyz789"
}

Your hook reads this, decides what to do, and outputs JSON to control Claude Code's behavior:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "🚨 [rm-home] rm targeting home directory"
  }
}

The permissionDecision can be "allow" (bypass permission, execute), "deny" (block, show reason to Claude), or "ask" (show permission dialog to user).

What Else Can You Build?

The hooks I've shared are just the starting point. Here's what's possible with 13 hook events:

The README in my repo has 12 more ideas. The potential is limited only by what you can script.

Tips from the Trenches

Get the Hooks

Everything I've described is open-source and ready to use:

claude-code-hooks

The repo includes ready-to-use hooks for safety, automation, and notifications. Plus a roadmap of ideas: auto-format, branch guards, cost tracking, Discord/ntfy notifications, session summaries, context injection, rate limiting, and more.

Star it, fork it, and contribute your own hooks. This is just the beginning.


Hooks are one of those features that seem minor but compound over time. Every blocked dangerous command saves a potential disaster. Every protected secret prevents a leak. Every auto-staged file saves a manual step.

Start with the event logger. See what's possible. Then add the safety hooks. They're the highest-value, lowest-effort wins. You'll wonder why you waited.

There's more to cover — prompt-based hooks, context injection, session memory, and advanced patterns. I'll dig into those in future posts.