Claude Code Hooks

Claude Code Hooks areuser-defined Shell commandsthat automatically execute at specific nodes in the Claude Code lifecycle.

With hooks, you can achieve precise control over Claude Code's behavior, ensuring that certain operations (such as code formatting, logging)are definitely triggered, rather than relying on the large model to autonomously decide whether to execute them.

Typical Application Scenarios of Hooks

Hooks can help you implement many practical features. Common scenarios include:

  • Message notification: Automatically send desktop/email reminders when Claude Code waits for input or needs permissions
  • Auto-formatting: After editing.tsfiles, automatically runprettier, after modifying.gofiles, executegofmt
  • Operation logs: Record all commands executed by Claude for compliance auditing or debugging/troubleshooting
  • Code standards validation: If the code generated by Claude does not comply with project standards (such as naming rules), automatically provide feedback
  • File permission control: Prevent Claude from modifying production environment configuration files or sensitive directories (such as.env、.git)

Compared to constraining Claude's behavior through prompts, hooks areapplication-level hard rulesthat are forcibly enforced as long as the corresponding event is triggered, providing higher stability and reliability.

Important Security Reminder

When hooks run, theydirectly use the credentials of the current system environment(such as environment variables, user permissions), which poses certain security risks:

  • Malicious hook code may leak your sensitive data (such as API keys, project source code)
  • Incorrect hook commands may cause accidental file deletion or system anomalies

Required Security Actions:

  1. Before registering hooks, be sure to review the logic and permissions of the commands line by line
  2. Avoid executing scripts of unknown origin in hooks
  3. For detailed security best practices, refer to the official documentation'sSecurity Precautions

Hook Event Type Description

Claude Code has multiple built-in lifecycle events. You can bind hook commands to different events. Each event passes different context data and affects Claude's behavior in different ways.

Event name Trigger timing Core function
PreToolUse Tool callBefore Can intercept tool execution (e.g., prevent modification of sensitive files) and provide feedback suggestions to Claude
PermissionRequest When a permission request dialog pops up Automatically approve or deny permission requests
PostToolUse Tool callAfter completion Perform post-operations (e.g., format code, log records)
UserPromptSubmit After the user submits a prompt, before Claude processes it Preprocess user input (e.g., supplement context information)
Notification When Claude sends notifications Customize notification methods (e.g., desktop pop-ups, SMS alerts)
Stop When Claude completes a response Perform finishing work (e.g., clean up temporary files)
SubagentStop When a subagent task completes Handle the execution results of the subagent
PreCompact When about to perform context compaction Customize compaction rules
SessionStart When starting a new session or resuming an old session Initialize the session environment (e.g., load project configuration)
SessionEnd When the session ends Save session data, clean up the environment

Quick Start: Implementing Command Logging

Below, usingrecording all Bash commands executed by Claudeas an example, we will walk you through configuring and using hooks step by step.

Prerequisites

Installjqtool (for parsing JSON data from the command line):

Step 1: Open the Hook Configuration Interface

In the Claude Code interactive interface, enter the slash command/hooks, press Enter, then select the event to bind — here we choosePreToolUse(triggered before tool calls, suitable for logging commands).

Step 2: Add an Event Matcher

The role of the matcher is tolimit the trigger conditions of the hook, only executing the hook command when the specified tool is called.

  1. Select+ Add new matcher…
  2. Enter a matching keywordBash, meaning the hook triggers only when Claude calls the Bash tool.

    Tip: Enter*can matchall tools, enabling a global hook.

Step 3: Add a Hook Command

Select+ Add new hook…, enter the following command (function: extract command content and description, write to a log file):

jq -r '"\(.tool_input.command) - \(.tool_input.description // "无描述信息")"' >> ~/.claude/bash-command-log.txt

Command description:

  • jq -r ...: Extract the command from JSON (command) and the description (description), display "no description information" when there is no description.
  • >> ~/.claude/...: Append the content to the log file in the user's home directory.

Step 4: Choose the Configuration Storage Location

The configuration save location determines the scope of the hook:

  • User settings: Save to user-level configuration (~/.claude/settings.json),effective for all projects
  • Project settings: Save to the current project configuration (.claude/settings.local.json),effective only for the current project

Here we chooseUser settings, to enable global command logging. After selecting, pressEsckey to exit the configuration interface, and the hook is registered.

Step 5: Verify the Hook Configuration

  1. Enter again/hookscommand to view the configured hook list
  2. or directly open the configuration file~/.claude/settings.json, and you will see the following configuration:
    {
      "hooks": {
        "PreToolUse": [
          {
            "matcher": "Bash",
            "hooks": [
              {
                "type": "command",
                "command": "jq -r '\"\\(.tool_input.command) - \\(.tool_input.description // \"无描述信息\")\"' >> ~/.claude/bash-command-log.txt"
              }
            ]
          }
        ]
      }
    }

Step 6: Test the Hook Effect

  1. Enter a command in Claude Code:帮我执行 ls 命令
  2. After execution, view the log file in the terminal:
    cat ~/.claude/bash-command-log.txt
  3. If the following content appears in the log, the hook is configured successfully:
    ls - Lists files and directories

Practical Hook Examples

Below are several commonly used hook configurations. You can copy them directly or modify them as needed.

📌 For complete example code, refer to the official repository:bash command validator example

Example 1: Automatically Format TypeScript Files

Function: After editing/writing.tsfiles, automatically useprettierto format the code

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write", // 匹配“编辑”和“写入”工具
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | { read file_path; if echo \"$file_path\" | grep -q '\\.ts$'; then npx prettier --write \"$file_path\"; fi; }"
          }
        ]
      }
    ]
  }
}

Example 2: Automatically Fix Markdown File Formatting

Function: Automatically add language tags to code blocks without them, and clean up extra blank lines

Step 1: Add hook configuration

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/markdown_formatter.py"
          }
        ]
      }
    ]
  }
}

Second step: Create the formatting script

In the project directory, create a new file.claude/hooks/markdown_formatter.py, then paste the following code:

Examples

#!/usr/bin/env python3
"""
Markdown formatting tool: automatically add language tags to code blocks, clean up extra blank lines
"""

import json
import sys
import re
import os

def detect_language(code):
    """Automatically detect programming language based on code content"""
    code = code.strip()
    # Detect JSON
    if re.search(r'^\s*[{\[]', code):
        try:
            json.loads(code)
            return 'json'
        except:
            pass
    # Detect Python
    if re.search(r'^\s*def\s+\w+\s*\(', code, re.M) or re.search(r'^\s*(import|from)\s+\w+', code, re.M):
        return 'python'
    # Detect JavaScript/TypeScript
    if re.search(r'\b(function\s+\w+\s*\(|const\s+\w+\s*=)', code) or re.search(r'=>|console\.(log|error)', code):
        return 'javascript'
    # Detect Bash
    if re.search(r'^#!.*\b(bash|sh)\b', code, re.M) or re.search(r'\b(if|then|fi|for|in|do|done)\b', code):
        return 'bash'
    # Default text format
    return 'text'

def format_markdown(content):
    """Format Markdown content"""
    # Add language to untagged code blocks
    fence_pattern = r'(?ms)^([ \t]{0,3})```([^\n]*)\n(.*?)(\n\1```)\s*$'
    def add_lang(match):
        indent, info, body, closing = match.groups()
        if not info.strip():
            lang = detect_language(body)
            return f"{indent}```{lang}\n{body}{closing}\n"
        return match.group(0)
    content = re.sub(fence_pattern, add_lang, content)
    # Clean up extra blank lines (only content outside code blocks)
    content = re.sub(r'\n{3,}', '\n\n', content)
    return content.rstrip() + '\n'

if __name__ == "__main__":
    try:
        # Read JSON data passed by Claude
        input_data = json.load(sys.stdin)
        file_path = input_data.get('tool_input', {}).get('file_path', '')
        # Process only .md/.mdx files
        if not file_path.endswith(('.md', '.mdx')):
            sys.exit(0)
        # Read and format the file
        if os.path.exists(file_path):
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
            formatted_content = format_markdown(content)
            # Write only when content changes
            if formatted_content != content:
                with open(file_path, 'w', encoding='utf-8') as f:
                    f.write(formatted_content)
                print(f"Formatted Markdown file: {file_path}")
    except Exception as e:
        print(f"Formatting failed: {e}", file=sys.stderr)
        sys.exit(1)

Third step: Grant execute permission to the script

chmod +x .claude/hooks/markdown_formatter.py

Example 3: Send Desktop Notifications When Claude Waits for Input

Feature: When Claude needs user input, automatically show a desktop reminder (works on Linux/macOS)

{
  "hooks": {
    "Notification": [
      {
        "matcher": "", // 匹配所有通知事件
        "hooks": [
          {
            "type": "command",
            "command": "notify-send 'Claude Code 提示' '请你输入指令或确认权限'"
          }
        ]
      }
    ]
  }
}

Example 4: Prevent Modifying Sensitive Files

Feature: Prevent Claude from editing.env、package-lock.jsonand other sensitive files

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "python3 -c \"import json, sys; data=json.load(sys.stdin); path=data.get('tool_input',{}).get('file_path',''); sys.exit(2 if any(p in path for p in ['.env', 'package-lock.json', '.git/']) else 0)\""
          }
        ]
      }
    ]
  }
}

Note: When the script returns a status code2, Claude Code will intercept this tool call, thereby preventing file modification.


Claude Code Hooks Reference Manual

Configuration File Path

Configuration level File path Scope
User-level ~/.claude/settings.json All projects
Project-level .claude/settings.json Current project
Local project-level (not committed) .claude/settings.local.json Current project, not included in version control
Managed policy level Admin-specified path Unified enterprise/team control

Core Configuration Structure

Hooks are organized byevent + matcher, supportingcommand(execute Shell commands) andprompt(call LLM for decisions), two types.

{
  "hooks": {
    "【钩子事件名】": [
      {
        "matcher": "【工具匹配规则】", // 部分事件可省略
        "hooks": [
          {
            "type": "command/prompt",
            "command": "【Shell 命令】", // type=command 时必填
            "prompt": "【LLM 提示词】",  // type=prompt 时必填
            "timeout": 30 // 可选,超时时间(秒)
          }
        ]
      }
    ]
  }
}

Matcher Rules (Only Applicable to Tool Events)

Matching rule Example Description
Exact match Write Match only theWritetool
Multi-tool match Edit | Write MatchEditorWritetools
Prefix match Notebook.* Match all tools starting withNotebookthe prefix
Full match */ empty string Match all tools

Special Configuration Tips

Scenario Configuration method Example
Reference in-project script Use environment variables$CLAUDE_PROJECT_DIR "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh"
Plugin Hooks Configured inside pluginhooks/hooks.json, using${CLAUDE_PLUGIN_ROOT}to reference plugin files "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh"
Component-level Hooks (Skill/Agent) Defined in component frontmatter, scope limited to component lifecycle See belowExtension configurationTable

Extended Configuration (Skill/Agent/Slash Commands)

Extension configuration allows Hooks to be embedded directly in the definition of a Skill, Agent, or custom slash command. These Hooks take effect only when the corresponding component is activated and running, and are automatically cleaned up after the component finishes execution, without affecting the global session.

Supported hook events

Only supportsPreToolUse、PostToolUse、Stopthree types of events, with the same functionality as global Hooks, but the scope is limited to the lifecycle of the current Skill/slash command.

Exclusive configuration option

  • once: true(optional): when set totrue, this Hook runs only once in the entire session, and is automatically removed after the first successful execution to avoid repeated triggering.

Complete configuration example

---
# Skill/斜杠命令的基础信息
name: secure-operations
description: 执行Shell命令前先做安全校验的工具
# Hooks 配置段
hooks:
  PreToolUse:
    # 匹配器:仅拦截Bash工具调用
    - matcher: "Bash"
      hooks:
        - type: "command"
          # 要执行的安全校验脚本
          command: "./scripts/security-check.sh"
          # 会话内仅执行一次
          once: true
          # 超时时间(秒),避免脚本卡死
          timeout: 15
---

Hooks Configuration in Agent

Supported hook events

Also only supportsPreToolUse、PostToolUse、StopThree types of events, scoped only to the task execution lifecycle of that sub-Agent.

Specific configuration items

No additional exclusive configuration items, does not supportonce: true(Hooks are triggered every time the Agent executes a task).

Complete configuration example

---
# Agent 的基础信息
name: code-reviewer
description: 自动审查代码修改并运行代码检查的子代理
# Hooks 配置段
hooks:
  PostToolUse:
    # 匹配器:拦截Edit(编辑)或Write(写入)工具
    - matcher: "Edit|Write"
      hooks:
        - type: "command"
          # 代码检查脚本,执行lint校验
          command: "./scripts/run-linter.sh"
          # 超时时间(秒)
          timeout: 30
---

Notes:

  1. The matcher rules for component-level Hooks are exactly the same as for global Hooks: exact matching is supported (e.g.,"Write"), multi-tool matching (e.g.,"Edit|Write"), wildcard matching ("*"), and matching is case-sensitive;
  2. Component-level Hooks and global Hooks execute in parallel: if both global and component-level Hooks are configured for the same event, both types of Hooks will run together when triggered without conflicting with each other;
  3. Configuration format requirements: component-level Hooks must be written in the frontmatter (---enclosed region), following YAML syntax; indentation errors will invalidate the configuration;
  4. Script path recommendations: prefer relative paths (e.g.,./scripts/xxx.sh), or use the$CLAUDE_PROJECT_DIRenvironment variable to specify an absolute path, ensuring the component can find the script in any directory.

Hook Events -- Tool Events (Matcher Supported)

Event name Trigger timing Common matchers Core purpose
PreToolUse Tool callbefore Bash/Edit/Write/Read Intercept tool execution, modify input parameters, auto-approve/deny permissions
PermissionRequest When a permission request dialog pops up samePreToolUse Automatically handle permission requests without manual user confirmation
PostToolUse Tool callAfter success samePreToolUse Execute post operations (e.g., code formatting, logging)
Notification When Claude sends a notification permission_prompt/idle_prompt/auth_success Customize notification methods (e.g., desktop popups, email alerts)
PreCompact Before performing context compression manual(Manual trigger) /auto(Automatic trigger) Customize compression rules, back up important context

Hook Events -- Session/Task Events (No Matcher)

Event name Trigger timing Core purpose
UserPromptSubmit After the user submits a prompt, before Claude processes it Validate prompt legitimacy, supplement context information
Stop When the main Agent completes its response (not triggered by user interruption) Intelligently determine whether to continue executing the task
SubagentStop When the sub-Agent task completes Evaluate subtask results, decide whether to terminate
SessionStart When starting/resuming a session Initialize environment, load project configuration, set persistent environment variables
SessionEnd When the session ends Clean up temporary files, record session logs, save working state

Hook Type Comparison

Claude Code supports two hook types to meet the needs of different scenarios.

Feature commandType (command hook) promptType (prompt hook)
Execution method Run Shell commands/scripts Call LLM (default Haiku model) for intelligent decision-making
Decision logic Based on code logic,deterministic judgment Based on context,flexible semantic judgment
Configuration difficulty Requires writing scripts, high entry barrier Only need to write prompts, simple and easy to use
Response speed Fast (local execution) Slower (requires API calls)
Applicable scenarios Fixed rules such as code formatting, logging, permission interception Flexible scenarios such as task completion evaluation, complex intent judgment
Core configuration command + timeout prompt + timeout(Can reference$ARGUMENTSplaceholders)

Input and Output -- Hook Input (JSON Passed via stdin)

All hooks receive common fields, and some events include specific fields.

Field type Common field Description
Basic information session_id Session unique identifier
transcript_path Conversation history file path
cwd Current working directory when the hook executes
permission_mode Current permission mode (default/plan/acceptEdits, etc.)
hook_event_name Currently triggered hook event name

Examples of event-specific fields for each event

Event name Specific fields Example
PreToolUse tool_name/tool_input {"tool_name":"Write","tool_input":{"file_path":"/test.txt"}}
UserPromptSubmit prompt {"prompt":"帮我写一个排序函数"}
SessionEnd reason {"reason":"clear/logout/other"}

Hook Output (Two Methods)

Method 1: Exit code (simple scenarios)

Pass execution status through the exit code,stdout/stderrUsed to provide feedback information.

Exit code Meaning Behavior description
0 Execution succeeded stdoutCan return JSON for advanced control; some events (e.g.,UserPromptSubmit) will addstdoutto the context
2 Block the operation Only usestderras the error message fed back to Claude,blocking the current event from continuing execution
Other non-zero values Non-blocking error stderrOnly displayed in verbose mode (ctrl+o), does not affect event execution

Behavior of exit code 2 in each event

Event name Triggered behavior
PreToolUse Block the tool call and display to Claudestderr
UserPromptSubmit Block prompt processing, erase user input
Stop Block Claude from stopping, force continued work
PostToolUse/Notification Only displaystderr, does not affect completed operations

Method 2: JSON output (advanced scenarios)

When the exit code is0, you canstdoutreturn JSON to achieve fine-grained control. The core fields are as follows:

Common JSON fields Purpose
continue: true/false Whether to allow the event to continue executing (falsetakes priority over other rules)
stopReason continue=falseReason shown to the user when
systemMessage Warning message displayed to the user

Examples of event-specific JSON fields

Event name Specific fields Example
PreToolUse permissionDecision(allow/deny/ask) {"hookSpecificOutput":{"permissionDecision":"allow","updatedInput":{"file_path":"/new.txt"}}}
UserPromptSubmit decision: block/undefined {"decision":"block","reason":"提示包含敏感信息"}
PostToolUse additionalContext {"hookSpecificOutput":{"additionalContext":"文件已格式化完成"}}

Hooks Configuration for MCP Tools

MCP tools can be seamlessly integrated with Hooks and matched through specific naming rules.

MCP Tool Naming Rules

mcp__<服务器名>__<工具名>

Example:mcp__github__search_repositories、mcp__filesystem__read_file

Matching Examples

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__memory__.*", // 匹配 memory 服务器的所有工具
        "hooks": [{"type": "command", "command": "echo '内存操作日志' >> log.txt"}]
      },
      {
        "matcher": "mcp__.*__write.*", // 匹配所有服务器的写操作工具
        "hooks": [{"type": "command", "command": "./validate-write.py"}]
      }
    ]
  }
}

Security and Debugging

Security Best Practices

Security key points Specific actions
Input validation Strictly validatetool_inputfile paths and command parameters in, to prevent path traversal (e.g.,../)
variable references Use in Shell commands"$VAR"instead of$VAR, to avoid parameter injection
Least privilege Hook scripts should only be granted necessary permissions; avoid usingsudoand other high-risk commands
Exclude sensitive files Intercept operations on.env、.gitand operations on key files
Command review Before registering a hook, manually execute the command to verify the logic and confirm there is no malicious behavior.

Debugging Tips

Problem type Troubleshooting steps
Hook does not take effect 1. Execute/hookscommand to check whether the configuration is registered<br>2. Verify that the JSON syntax is correct<br>3. Check whether the matcher rule matches the tool name (case-sensitive)
Command execution failed 1. Manually run the hook command to confirm it executes properly<br>2. Check whether the script has executable permissions (chmod +x script.sh)<br>3. Use an absolute path to call the script to avoid environment variable issues
View detailed logs Add when starting Claude Code--debugparameter to view the entire hook execution process

Reference documentation: For the complete hook feature description, refer to the officialHooks reference documentation。

Other extensions