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:
- Before registering hooks, be sure to review the logic and permissions of the commands line by line
- Avoid executing scripts of unknown origin in hooks
- 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):
- macOS:
brew install jq - Linux:
sudo apt install jq/sudo yum install jq - Windows: Downloadthe official jq installation package, or install via WSL
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.
- Select
+ Add new matcher… - Enter a matching keyword
Bash, 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
- Enter again
/hookscommand to view the configured hook list - 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
- Enter a command in Claude Code:
帮我执行 ls 命令 - After execution, view the log file in the terminal:
cat ~/.claude/bash-command-log.txt
- 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
"""
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:
- 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; - 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;
- Configuration format requirements: component-level Hooks must be written in the frontmatter (
---enclosed region), following YAML syntax; indentation errors will invalidate the configuration; - 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 |
Other extensionsReference documentation: For the complete hook feature description, refer to the officialHooks reference documentation。