Library · Hooks and helper agents

Hooks: automatic rules for Claude Code

Builder50 minUpdated: October 2026
35 of 105 in the library

Module: 7. Hooks: system-level autonomy | Time: about 30 min reading + 20 min practice


The gist

You call skills yourself, when you need them. Hooks work without you, all the time. They're like the rules in an employment contract: an employee follows them automatically without being reminded every time. A hook fires on a specific event (before an action, after it, on an error, on completion) and does what you told it to do.

This lesson gives you the full picture: about 30 event types, 5 handler types, the exact data format. Let's start with the main thing.


Key concepts

  • Hook: an automatic rule that fires on a specific event in Claude Code
  • Event: a moment in Claude Code's lifecycle: session start, a tool call, completion, and so on
  • Handler: what exactly runs on the event: a bash script, an HTTP request, an MCP tool, a prompt or an agent
  • Matcher: a filter for which tools or events to react to
  • settings.json: the hook configuration file (.claude/settings.json for a project, ~/.claude/settings.json globally)
  • Exit code: how a hook reports its decision: 0 = OK, 2 = block

Theory

Skills vs. hooks: what's the difference

They don't compete. They're different tools.

Skills Hooks
Activation You call them explicitly Automatically on an event
Scope Project or global Project or global
Where they live .claude/skills/<name>/SKILL.md .claude/settings.json or ~/.claude/settings.json
Purpose Instructions for how to do a task Safety and automation rules
Analogy A recipe The rules in an employment contract

🎨 Picture this: a hook is the security guard at the entrance and the exit. A skill is a specialist you hire for a specific job. The guard is always on duty. The specialist shows up when needed.


Event types: when hooks fire

Claude Code supports about 30 event types (the exact list grows from version to version; see the official documentation). To start, you need the 6 main ones. The rest are for advanced scenarios.

The 6 main events (80% of use)

Event When Why
PreToolUse BEFORE a tool runs Blocking dangerous actions, checking conditions
PostToolUse AFTER it runs successfully Logging, auditing, notifications
Stop Claude finished its response A "done" notification, cleanup, running tests
Notification Claude sends a notification Reacting to in-between events
SessionStart A session starts or resumes Loading context, checking the environment
UserPromptSubmit The user sent a request Validation, adding context before processing

Advanced events (for when you outgrow the main ones)

Event When Example
SubagentStart A subagent starts Logging which agents get started
SubagentStop A subagent finished its work Checking the subagent's result
PostToolUseFailure A tool ended with an error Sending an alert on an error
PostToolBatch A batch of parallel calls finished Checks after batch operations
FileChanged A file changed on disk Reloading .env when it changes
ConfigChange The configuration changed Reacting to a settings update
PreCompact Before the context is compressed Saving what matters before compaction
SessionEnd The session is ending Final cleanup, saving state
StopFailure A response was cut off by an API error An alert on a rate limit or billing error
PermissionRequest A permission prompt appeared Auto-approving certain operations
CwdChanged The working folder changed Switching environments
Setup Launch with --init or --maintenance Installing dependencies on initialization

🎨 Picture this: events are security cameras in a factory. A camera at the entrance (PreToolUse), a camera at the exit (PostToolUse), a camera in the director's office (Stop). You don't install thirty cameras on day one; you start with 3-4 at the critical points.


A closer look: the 4 main events

⚠️ A note on how current this is: early material about Claude Code mentions "4 types of hooks" (Pre-tool / Post-tool / Stop / Need you). That's the basic model from the old documentation. By October 2026 the ecosystem had grown to about 30 lifecycle events (PreToolUse, PostToolUse, UserPromptSubmit, Stop, SessionStart, SubagentStop, Notification, PreCompact, PostCompact and others) and 5 handler types (command, http, mcp_tool, prompt, agent). These 4 basic scenarios still cover most tasks. The other events are for fine-tuning on top. More in the Hook-Deny-By-Design lesson, which puts the advanced events to work.

Mapping the old "big four" to today's events:

Old category Today's events, 2026
Pre-tool PreToolUse + PreCompact + UserPromptSubmit
Post-tool PostToolUse + PostCompact + SessionStart
Stop Stop + SubagentStop
Need you Notification + UserPromptSubmit

🎨 Picture this: the old "big four" are four guard posts at a small warehouse. Today the warehouse has grown into a factory with thirty posts, but the 4 main entrances still handle most of the traffic. The rest are for special corridors.


🎨 Picture this: PreToolUse is quality control on an assembly line. The part isn't bolted on yet, but it's already being checked. You catch the defect before it's installed. Install a defective part and you have to take the whole unit apart.

1. PreToolUse: a check before the action

When it fires: before Claude runs any tool (writing a file, reading, a bash command, etc.)

Why: to block dangerous actions, check conditions, protect sensitive files.

Practical scenarios:

  • Keep Claude from editing the .env file with your API keys
  • Check that code doesn't contain hardcoded secrets
  • Block writes to the production database
  • Check the budget before expensive operations
json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/check-secrets.sh"
          }
        ]
      }
    ]
  }
}

If the script returns exit code 2 → Claude Code stops and doesn't perform the action. The message from stderr is passed to Claude.


🎨 Picture this: PostToolUse is the clerk in the records office. A document is signed and handed in, and the clerk logs it: who, what, when. Without that clerk, a month later you won't remember which files Claude touched on Monday.

2. PostToolUse: an action after it runs

When it fires: after Claude successfully runs a tool.

Why: to log what changed, build an audit trail, send notifications about specific changes.

Practical scenarios:

  • Write to a log file which files Claude changed and when
  • Send a notification to Slack or Telegram when a critical file changes
  • Update an operations counter for budget control
  • Create a git commit after changes
json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/audit-log.sh"
          }
        ]
      }
    ]
  }
}

3. Stop: when the response is done

When it fires: when Claude Code finishes responding and completes the task.

Why: to let you know the work is done, clean up, kick off the next step.

Practical scenarios:

  • A macOS notification "Claude finished the task", so you can work on something else in the meantime
  • Sending a final report to a messaging app
  • Running tests after Claude has written code
  • An automatic git commit at the end
json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude finished the task\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

🎨 Picture this: a Stop hook = a courier who calls to say "your order's been delivered." You don't stand by the door all day; you wait for the call.


4. Notification: keeping you informed

When it fires: when Claude Code sends the user a notification (other than Stop).

Why: to react to Claude's in-between messages, not just to completion.

How it differs from Stop: Stop is the full completion of a task. Notification is Claude telling you something along the way.

Matcher options: permission_prompt, idle_prompt, auth_success

Practical scenarios:

  • Log all of Claude's in-between messages
  • Get notified when Claude hits an error and keeps working
  • Track the progress of long tasks

5 handler types: HOW a hook does its job

The event is WHEN. The handler is HOW. Claude Code supports 5 handler types:

Type What it does When to use it
command Runs a bash script 90% of cases: checks, logs, notifications
http Sends an HTTP POST request A webhook to Slack, Telegram or an external service
mcp_tool Calls a tool on an MCP server When an MCP server is already connected
prompt Sends text to a fast model An AI check of a request before it runs
agent Starts a subagent (experimental) Complex checks that need reasoning

🎨 Picture this: 5 handlers = 5 ways a security guard can react. Check it himself (command), call the boss (http), use the radio (mcp_tool), ask a partner (prompt), call in the response team (agent).

The command handler (a bash script): the main one

The simplest and most common. It runs a shell script.

json
{
  "type": "command",
  "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-secrets.sh",
  "timeout": 30
}

The http handler (a webhook): for external services

Sends the hook's data as a POST request. The request body is the same JSON a command hook gets through stdin.

json
{
  "type": "http",
  "url": "http://localhost:8080/hooks/validate",
  "headers": {
    "Authorization": "Bearer $MY_TOKEN"
  },
  "allowedEnvVars": ["MY_TOKEN"],
  "timeout": 30
}

The server's JSON response is handled the same way as a command hook's stdout.

The prompt handler: a quick AI check

Sends text to a fast model. Useful for judging whether a request is safe.

json
{
  "type": "prompt",
  "prompt": "Is this bash command safe? Command: $ARGUMENTS\nRespond with JSON: {\"decision\": \"allow\"} or {\"decision\": \"deny\"}",
  "timeout": 30
}

The mcp_tool and agent handlers: advanced

mcp_tool calls a tool on a connected MCP server. agent starts a subagent to do the check (the documentation marks it as experimental). Both are for complex scenarios, not for getting started.


Matcher: the "what to react to" filter

🎨 Picture this: a matcher is the filter at the security desk. The guard doesn't stop everyone, only people carrying boxes (Write|Edit). Couriers go through without a check. Otherwise the line at the door would stretch down the block.

The matcher defines which SPECIFIC tools to react to. Without a matcher, the hook fires on EVERYTHING.

Matcher value What it does Example
"Bash" Bash commands only The hook fires on npm test, git push
"Write|Edit" Writing or editing files A hook that checks for secrets
"mcp__memory__.*" All tools of the memory MCP server Auditing MCP operations
"*" or missing All tools A universal log

A matcher is a regular expression if it contains special characters, or an exact match if it's only letters.

An extra "if" filter lets you filter by arguments (for example, Bash(git *) or Edit(*.ts)):

json
{
  "matcher": "Bash",
  "hooks": [{
    "type": "command",
    "if": "Bash(rm *)",
    "command": "echo 'rm is blocked' >&2 && exit 2"
  }]
}

Here the hook fires only for Bash, and only if the command starts with rm. The full if syntax is in the official hooks reference.


The structure of settings.json (official format)

All hooks live in settings.json. There are three levels of files:

File Scope Share it?
~/.claude/settings.json All projects (global) No
.claude/settings.json This project Yes (commit it to git)
.claude/settings.local.json This project (local) No (in .gitignore)

The structure: 3 levels of nesting

Code
hooks → Event → [{ matcher, hooks: [{ type, command, ... }] }]

A full example with 3 hooks:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-secrets.sh",
            "timeout": 30
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/audit-log.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

Key rules:

  • Event names are CamelCase: PreToolUse, not pre_tool_use
  • Each event holds an array of groups with matcher and hooks
  • Each group holds an array of hooks handlers
  • matcher filters by tool (not needed for Stop/SessionStart)
  • You can turn off all hooks entirely: "disableAllHooks": true

How a hook gets its data (the JSON protocol)

Claude Code passes data to the hook through stdin (for command hooks) or the POST body (for http hooks), in JSON format.

What a PreToolUse hook gets

json
{
  "session_id": "abc123",
  "cwd": "/Users/me/project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/project/config.py",
    "content": "API_KEY = 'sk-proj-abc123...'"
  }
}

What a PostToolUse hook gets

json
{
  "session_id": "abc123",
  "cwd": "/Users/me/project",
  "hook_event_name": "PostToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "npm test" },
  "tool_response": "All tests passed"
}

What a SessionStart hook gets

json
{
  "session_id": "abc123",
  "cwd": "/Users/me/project",
  "hook_event_name": "SessionStart",
  "source": "startup",
  "model": "<model identifier>"
}

How a hook answers Claude Code (the JSON response)

A hook can return JSON through stdout to control behavior:

json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "Safe command",
    "additionalContext": "A hint for Claude"
  }
}

Decisions for PreToolUse: "allow" (allow without asking), "deny" (block), "ask" (ask the user); newer versions also have "defer".

If several hooks give different decisions, the priority is: deny > ask > allow.

Exit codes: how a hook reports its decision

Exit code Result
0 Success. Claude Code parses stdout as JSON
2 Block. Stderr is passed to Claude as the reason
1 or anything else A non-blocking error: it gets logged and work continues

Important: exit code 2 (not 1!) blocks the action. Exit code 1 just means an error: the hook "broke," but Claude keeps working.


How to add a hook: two ways

Way 1: Ask Claude Code (recommended to start)

Type this into the chat
I want to make a hook: when Claude finishes a response,
send me a macOS notification

Claude Code will ask clarifying questions, create a bash script and add the entry to settings.json.

Way 2: Through /hooks in the terminal

bash
claude
# In the Claude Code interface:
/hooks
# Opens the list of configured hooks (view only)
# To edit, change settings.json directly

It shows the current hooks: the handler type ([command], [http], [prompt]), the source ([User], [Project], [Local]) and the matcher.


Global vs. project hooks

🎨 Picture this: global hooks are like fire safety rules. They apply in any building you walk into. Project hooks are like the instructions for a specific site: "At this warehouse, also check the temperature."

Put safety hooks (secrets, budget) in the global file (~/.claude/settings.json). They protect you in every project.

Put project-specific hooks (linter, tests, deployment) in the project file (.claude/settings.json). You can commit them to git and share them with your team.

Code
~/.claude/settings.json          ← Safety (all projects)
  └── PreToolUse: no-secrets
  └── PreToolUse: budget-check

.claude/settings.json            ← Project (this project)
  └── PostToolUse: run-linter
  └── Stop: run-tests

All levels are combined. Global + project + local hooks work together.


Environment variables in hooks

Inside a command hook you have access to:

Variable What it holds
$CLAUDE_PROJECT_DIR The project root (wrap it in quotes!)
$CLAUDE_ENV_FILE A path for saving env variables for the whole session

Example:

bash
#!/bin/bash
# Run a script from the project folder
"$CLAUDE_PROJECT_DIR"/.claude/hooks/my-check.sh

Practice

Task: get to know the structure of settings.json

  1. Open or create the file .claude/settings.json
  2. Ask Claude Code: Show me the current hook settings
  3. Ask it to create the simplest possible hook: Create a hook: when Claude finishes a response, show a "Done" notification, and click yes
  4. Check that settings.json was updated: look for the new entry in the Stop section (CamelCase!)
  5. Test it: ask Claude any simple question, and the notification should appear
  6. Type /hooks in Claude Code and make sure the hook shows up in the list

Goal: understand that settings.json is the single place where all hooks are configured, and that hooks work automatically without you


Tools and resources

  • .claude/settings.json: the project hooks file (commit it to git)
  • ~/.claude/settings.json: the global hooks file (all projects)
  • /hooks: the command for viewing configured hooks in Claude Code
  • jq: a tool for parsing JSON in bash scripts (needed for command hooks)
  • osascript: the macOS command for sending native notifications
  • Official documentation (current reference): https://code.claude.com/docs/en/hooks — all lifecycle events, JSON formats, exit codes
  • Advanced events: the Hook-Deny-By-Design lesson, with SubagentStop, PreCompact and PermissionRequest in practice

Sources


Key takeaways

Hooks ≠ skills. You call skills yourself. Hooks work automatically on an event, and you set them up once.

There are about 30 event types, but start with the 4-6 main ones: PreToolUse, PostToolUse, Stop, Notification, SessionStart, UserPromptSubmit.

5 handler types: command (bash), http (webhook), mcp_tool, prompt (an AI check), agent (a subagent). For getting started, command is enough.

The matcher filters by tool: "Write|Edit" means file operations only, "Bash" means commands only.

Exit code 2 = block, exit code 0 = allow. It's 2, not 1, that blocks!

Hooks live in settings.json at three levels: global, project and local. They all get combined.


What's next

→ Hooks LIVE: building hooks from scratch

The mark stays in this browser only and is never sent anywhere. My progress