2.8 Hooks:自动化工作流的钩子系统
什么是 Hooks
Hooks 是用户定义的 Shell 命令,在 Claude Code 生命周期的特定时刻自动执行。
与 Slash Commands 和 Skills 不同,Hooks 提供了确定性控制——它们不依赖 LLM 的判断,而是在特定事件发生时必然执行。
核心价值
| 特性 | 说明 |
|---|---|
| 确定性执行 | 不依赖 AI 判断,每次都会执行 |
| 自动化 | 无需手动触发,事件驱动 |
| 质量保障 | 自动格式化、验证、安全检查 |
| 合规审计 | 自动记录所有操作日志 |
| 权限控制 | 阻止危险操作,保护敏感文件 |
与其他扩展方式对比
| 维度 | Hooks | Slash Commands | Skills | Subagent |
|---|---|---|---|---|
| 触发方式 | 自动(事件驱动) | 手动(/命令) | 自动/手动 | 手动/自动 |
| 执行确定性 | 100% 确定 | 用户决定 | AI 判断 | AI 判断 |
| 主要用途 | 自动化规则 | 快捷操作 | 复杂流程 | 独立任务 |
| 配置方式 | JSON 配置 | Markdown | 文件夹结构 | Markdown |
Hook 事件类型
Claude Code 支持以下 Hook 事件:
┌─────────────────────────────────────────────────────────────────┐
│ Claude Code 生命周期 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ SessionStart │────▶│ 用户交互循环 │────▶│ SessionEnd │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ UserPromptSubmit │ │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 工具调用循环 │ │
│ │ ┌──────────┐ ┌──────────────────┐ │ │
│ │ │PreToolUse│─▶│PermissionRequest │ │ │
│ │ └──────────┘ └────────┬─────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ PostToolUse │ │ │
│ │ └─────────────┘ │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Notification │ │ Stop │ │ SubagentStop │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ │
│ │ PreCompact │ (上下文压缩前) │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘事件详解
| Hook 事件 | 触发时机 | 可控制行为 | 常见用途 |
|---|---|---|---|
| PreToolUse | 工具调用前 | 允许/拒绝/修改参数 | 命令验证、安全检查 |
| PermissionRequest | 显示权限对话框时 | 自动允许/拒绝 | 自动授权已知安全操作 |
| PostToolUse | 工具调用完成后 | 添加上下文反馈 | 代码格式化、日志记录 |
| UserPromptSubmit | 用户提交提示前 | 阻止/添加上下文 | 敏感信息检查、上下文注入 |
| Notification | 发送通知时 | 自定义通知方式 | 桌面通知、声音提醒 |
| Stop | Claude 完成响应时 | 强制继续执行 | 任务完成验证 |
| SubagentStop | 子代理任务完成时 | 强制继续执行 | 子任务验证 |
| SessionStart | 会话启动/恢复时 | 注入环境变量 | 环境初始化 |
| SessionEnd | 会话结束时 | 无法阻止 | 清理、日志保存 |
| PreCompact | 执行压缩操作前 | 添加自定义指令 | 保留重要上下文 |
配置文件位置
Hooks 通过 JSON 配置文件设置,支持三个位置:
| 位置 | 文件路径 | 适用范围 | 是否提交 Git |
|---|---|---|---|
| 用户级 | ~/.claude/settings.json | 所有项目 | 否 |
| 项目级 | .claude/settings.json | 当前项目 | 是(推荐) |
| 本地项目 | .claude/settings.local.json | 当前项目 | 否 |
优先级: 本地项目 > 项目级 > 用户级
快速开始
方法一:使用 /hooks 命令(推荐)
# 在 Claude Code 中执行
> /hooks- 选择 Hook 事件(如
PreToolUse) - 添加匹配器(如
Bash或*) - 输入 Hook 命令
- 选择保存位置
方法二:直接编辑配置文件
创建或编辑 ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command' >> ~/.claude/bash-commands.log"
}
]
}
]
}
}配置语法详解
基本结构
{
"hooks": {
"事件名称": [
{
"matcher": "匹配模式",
"hooks": [
{
"type": "command",
"command": "要执行的命令",
"timeout": 60
}
]
}
]
}
}匹配器(Matcher)语法
| 模式 | 说明 | 示例 |
|---|---|---|
| 精确匹配 | 匹配特定工具 | "Write" |
| 正则表达式 | 匹配多个工具 | "Edit|Write" |
| 通配符 | 匹配所有 | "*" 或 "" |
| MCP 工具 | 匹配 MCP 服务器工具 | "mcp__memory__.*" |
Hook 类型
1. Command 类型(Shell 命令)
{
"type": "command",
"command": "your-shell-command",
"timeout": 60
}2. Prompt 类型(LLM 评估)
仅适用于特定事件:Stop、SubagentStop、UserPromptSubmit、PreToolUse、PermissionRequest
{
"type": "prompt",
"prompt": "评估是否应该继续执行。检查所有任务是否完成。",
"timeout": 30
}环境变量
Hook 命令可以访问以下环境变量:
| 变量 | 说明 |
|---|---|
$CLAUDE_PROJECT_DIR | 项目根目录路径 |
$CLAUDE_CODE_REMOTE | 是否远程运行("true" 或 "false") |
$CLAUDE_ENV_FILE | 环境变量持久化文件(仅 SessionStart) |
${CLAUDE_PLUGIN_ROOT} | 插件根目录(仅插件 Hook) |
输入输出格式
Hook 接收的输入(stdin)
Hook 通过标准输入(stdin)接收 JSON 格式数据:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/directory",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/file",
"content": "文件内容..."
},
"tool_use_id": "toolu_01ABC123..."
}Hook 输出(stdout)和退出码
| 退出码 | 含义 | 输出处理 |
|---|---|---|
| 0 | 成功 | JSON 输出被解析用于控制决策 |
| 2 | 阻塞错误 | 仅 stderr 作为错误消息显示 |
| 其他 | 非阻塞错误 | stderr 在详细模式显示 |
输出 JSON 结构
{
"decision": "block",
"reason": "说明原因",
"continue": false,
"stopReason": "显示给用户的消息",
"suppressOutput": true,
"systemMessage": "警告信息",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "拒绝原因"
}
}实战案例
案例 1:Bash 命令日志记录
用途: 记录所有执行的 Bash 命令用于审计
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '\"[\" + (now | todate) + \"] \" + .tool_input.command' >> ~/.claude/bash-audit.log"
}
]
}
]
}
}案例 2:自动代码格式化
用途: 文件修改后自动运行 Prettier
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs -I {} sh -c 'echo {} | grep -qE \"\\.(ts|tsx|js|jsx)$\" && npx prettier --write {} 2>/dev/null || true'"
}
]
}
]
}
}案例 3:敏感文件保护
用途: 阻止修改敏感文件(.env、密钥文件等)
{
"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',''); blocked=['.env', '.env.local', 'credentials', 'secret', '.git/']; sys.exit(2) if any(b in path.lower() for b in blocked) else sys.exit(0)\""
}
]
}
]
}
}案例 4:自定义桌面通知
用途: Claude 等待输入时发送桌面通知
macOS 版本:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code 等待您的输入\" with title \"Claude Code\"'"
}
]
}
]
}
}Linux 版本:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' '等待您的输入'"
}
]
}
]
}
}案例 5:Bash 命令验证器
用途: 验证 Bash 命令,推荐使用更好的工具
创建脚本 .claude/hooks/bash_validator.py:
#!/usr/bin/env python3
"""
Bash 命令验证器
检查命令并提供更好的替代方案
"""
import json
import sys
import re
def validate_command(command):
"""验证命令并返回建议"""
suggestions = []
# 推荐使用 rg 代替 grep
if re.search(r'\bgrep\b(?!.*\|)', command):
suggestions.append("建议使用 'rg'(ripgrep)代替 'grep',性能更好")
# 推荐使用专用工具读取文件
if re.search(r'\bcat\s+\S+\s*$', command):
suggestions.append("建议使用 Read 工具代替 'cat' 读取文件")
# 检查危险命令
dangerous_patterns = [
(r'\brm\s+-rf\s+/', "警告:这是危险的删除操作"),
(r'\bsudo\b', "警告:sudo 命令需要谨慎使用"),
(r'>\s*/dev/sd', "警告:直接写入磁盘设备"),
]
for pattern, message in dangerous_patterns:
if re.search(pattern, command):
suggestions.append(message)
return suggestions
try:
input_data = json.load(sys.stdin)
tool_name = input_data.get("tool_name", "")
command = input_data.get("tool_input", {}).get("command", "")
if tool_name != "Bash":
sys.exit(0)
suggestions = validate_command(command)
if suggestions:
# 输出建议但不阻止执行
output = {
"systemMessage": "\\n".join(suggestions)
}
print(json.dumps(output))
sys.exit(0)
except Exception as e:
print(f"验证错误: {e}", file=sys.stderr)
sys.exit(1)配置文件:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/bash_validator.py\""
}
]
}
]
}
}案例 6:用户输入敏感信息检测
用途: 阻止用户在提示中包含敏感信息
创建脚本 .claude/hooks/sensitive_detector.py:
#!/usr/bin/env python3
"""
敏感信息检测器
检查用户输入中的密码、密钥等敏感信息
"""
import json
import sys
import re
def check_sensitive(text):
"""检查文本中的敏感信息"""
patterns = [
(r'(?i)(password|passwd|pwd)\s*[:=]\s*\S+', "检测到密码信息"),
(r'(?i)(api[_-]?key|apikey)\s*[:=]\s*\S+', "检测到 API 密钥"),
(r'(?i)(secret|token)\s*[:=]\s*\S+', "检测到密钥/令牌"),
(r'(?i)(aws[_-]?access|aws[_-]?secret)', "检测到 AWS 凭证"),
(r'sk-[a-zA-Z0-9]{20,}', "检测到 OpenAI API 密钥格式"),
(r'ghp_[a-zA-Z0-9]{36}', "检测到 GitHub 个人令牌"),
]
for pattern, message in patterns:
if re.search(pattern, text):
return message
return None
try:
input_data = json.load(sys.stdin)
prompt = input_data.get("prompt", "")
warning = check_sensitive(prompt)
if warning:
output = {
"decision": "block",
"reason": f"安全警告:{warning}。请移除敏感信息后重试。"
}
print(json.dumps(output))
sys.exit(0)
sys.exit(0)
except Exception as e:
sys.exit(0) # 出错时不阻止用户输入配置文件:
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/sensitive_detector.py\""
}
]
}
]
}
}案例 7:Markdown 自动格式化
用途: 自动为 Markdown 代码块添加语言标签
创建脚本 .claude/hooks/markdown_formatter.py:
#!/usr/bin/env python3
"""
Markdown 格式化器
自动检测并添加代码块语言标签
"""
import json
import sys
import re
import os
def detect_language(code):
"""根据代码内容检测编程语言"""
s = code.strip()
# JSON 检测
if re.search(r'^\s*[{\[]', s):
try:
json.loads(s)
return 'json'
except:
pass
# Python 检测
if re.search(r'^\s*def\s+\w+\s*\(', s, re.M) or \
re.search(r'^\s*(import|from)\s+\w+', s, re.M) or \
re.search(r'^\s*class\s+\w+', s, re.M):
return 'python'
# JavaScript/TypeScript 检测
if re.search(r'\b(function\s+\w+\s*\(|const\s+\w+\s*=|let\s+\w+\s*=)', s) or \
re.search(r'=>|console\.(log|error)', s):
return 'javascript'
# Bash 检测
if re.search(r'^#!.*\b(bash|sh)\b', s, re.M) or \
re.search(r'^\s*(if|then|fi|for|in|do|done|echo)\b', s, re.M):
return 'bash'
# SQL 检测
if re.search(r'\b(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER)\s+', s, re.I):
return 'sql'
# HTML 检测
if re.search(r'<\/?[a-z][\s\S]*>', s, re.I):
return 'html'
# CSS 检测
if re.search(r'[.#][\w-]+\s*\{', s):
return 'css'
return 'text'
def format_markdown(content):
"""格式化 Markdown 内容"""
def add_lang_to_fence(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)
# 修复未标记的代码块
fence_pattern = r'(?ms)^([ \t]{0,3})```([^\n]*)\n(.*?)(\n\1```)\s*$'
content = re.sub(fence_pattern, add_lang_to_fence, content)
# 修复多余空行(代码块外)
content = re.sub(r'\n{3,}', '\n\n', content)
return content.rstrip() + '\n'
try:
input_data = json.load(sys.stdin)
file_path = input_data.get('tool_input', {}).get('file_path', '')
if not file_path.endswith(('.md', '.mdx')):
sys.exit(0)
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
formatted = format_markdown(content)
if formatted != content:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(formatted)
print(f"✓ 已格式化 {file_path}")
except Exception as e:
print(f"格式化错误: {e}", file=sys.stderr)
sys.exit(1)案例 8:会话环境初始化
用途: 会话开始时自动设置环境变量
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "if [ -n \"$CLAUDE_ENV_FILE\" ]; then echo 'export NODE_ENV=development' >> \"$CLAUDE_ENV_FILE\"; echo 'export DEBUG=true' >> \"$CLAUDE_ENV_FILE\"; fi"
}
]
}
]
}
}案例 9:Git 分支保护
用途: 阻止在 main/master 分支上直接修改文件
创建脚本 .claude/hooks/branch_protector.py:
#!/usr/bin/env python3
"""
Git 分支保护器
阻止在主分支上直接修改代码
"""
import json
import sys
import subprocess
def get_current_branch():
"""获取当前 Git 分支"""
try:
result = subprocess.run(
['git', 'branch', '--show-current'],
capture_output=True,
text=True,
timeout=5
)
return result.stdout.strip()
except:
return None
def main():
try:
input_data = json.load(sys.stdin)
tool_name = input_data.get("tool_name", "")
# 只检查文件修改操作
if tool_name not in ["Edit", "Write"]:
sys.exit(0)
branch = get_current_branch()
protected_branches = ['main', 'master', 'production', 'prod']
if branch in protected_branches:
output = {
"decision": "block",
"reason": f"⚠️ 当前在受保护分支 '{branch}' 上。请先创建功能分支:git checkout -b feature/your-feature"
}
print(json.dumps(output))
sys.exit(0)
sys.exit(0)
except Exception as e:
sys.exit(0)
if __name__ == "__main__":
main()配置文件:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/branch_protector.py\""
}
]
}
]
}
}MCP 工具 Hook
MCP(Model Context Protocol)工具可以通过特定模式匹配:
mcp__<服务器名>__<工具名>示例配置
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__memory__.*",
"hooks": [
{
"type": "command",
"command": "echo '[Memory] 操作记录' >> ~/.claude/mcp-operations.log"
}
]
},
{
"matcher": "mcp__filesystem__.*",
"hooks": [
{
"type": "command",
"command": "echo '[Filesystem] 文件操作' >> ~/.claude/mcp-operations.log"
}
]
}
]
}
}安全最佳实践
⚠️ 重要安全警告
Hooks 在您的环境凭证下自动运行。恶意 Hook 代码可能窃取数据或破坏系统。务必在启用前仔细审查所有 Hook 实现。
安全检查清单
| 检查项 | 说明 |
|---|---|
| ✅ 验证输入 | 永远不要盲目信任输入数据 |
| ✅ 引用变量 | 使用 "$VAR" 而非 $VAR |
| ✅ 阻止路径遍历 | 检查路径中的 .. |
| ✅ 使用绝对路径 | 使用完整路径或 $CLAUDE_PROJECT_DIR |
| ✅ 跳过敏感文件 | 避免处理 .env、.git/、密钥文件 |
| ✅ 限制超时 | 设置合理的 timeout 值 |
| ✅ 审查第三方 Hook | 使用前检查所有外部 Hook 代码 |
安全示例:输入验证
#!/usr/bin/env python3
import json
import sys
import os
data = json.load(sys.stdin)
file_path = data.get('tool_input', {}).get('file_path', '')
# 1. 验证路径不为空
if not file_path:
sys.exit(0)
# 2. 检查路径遍历
if '..' in file_path:
print("错误:检测到路径遍历", file=sys.stderr)
sys.exit(2)
# 3. 验证在项目目录内
project_dir = os.environ.get('CLAUDE_PROJECT_DIR', '')
if project_dir and not os.path.abspath(file_path).startswith(project_dir):
print("错误:文件在项目目录外", file=sys.stderr)
sys.exit(2)
sys.exit(0)调试技巧
启用详细输出
# 使用 --debug 标志
claude --debug
# 或在运行时按 Ctrl+O 切换详细模式验证配置
# 查看当前 Hook 配置
> /hooks
# 或直接查看配置文件
cat ~/.claude/settings.json | jq '.hooks'常见问题排查
| 问题 | 解决方案 |
|---|---|
| Hook 不执行 | 检查 matcher 模式是否正确(区分大小写) |
| JSON 解析错误 | 验证 stdin 输入格式,使用 jq 调试 |
| 权限错误 | 检查脚本执行权限:chmod +x script.py |
| 超时 | 增加 timeout 值或优化脚本性能 |
| 退出码错误 | 确保成功返回 0,阻止返回 2 |
与 Plugin 集成
Plugin 可以包含 Hooks,存放在 hooks/hooks.json 中:
your-plugin/
├── plugin.json
├── hooks/
│ └── hooks.json
└── scripts/
└── format.shhooks.json 示例:
{
"description": "自动代码格式化",
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
"timeout": 30
}
]
}
]
}
}实用技巧总结
1. Hook 执行顺序
- 所有匹配的 Hook 并行执行
- 相同的 Hook 只执行一次(自动去重)
- 默认超时 60 秒
2. 推荐的 Hook 脚本目录结构
.claude/
├── hooks/
│ ├── bash_validator.py # Bash 命令验证
│ ├── sensitive_detector.py # 敏感信息检测
│ ├── branch_protector.py # 分支保护
│ └── markdown_formatter.py # Markdown 格式化
├── settings.json # Hook 配置
└── settings.local.json # 本地配置(不提交)3. 使用 jq 处理 JSON
# 提取文件路径
jq -r '.tool_input.file_path'
# 提取命令
jq -r '.tool_input.command'
# 添加时间戳
jq -r '"[" + (now | todate) + "] " + .tool_input.command'本节小结
- Hooks = 自动化规则引擎 — 确定性执行,不依赖 AI 判断
- 10 种事件类型 — 覆盖完整的 Claude Code 生命周期
- 三个配置位置 — 用户级、项目级、本地项目级
- 两种 Hook 类型 — Command(Shell)和 Prompt(LLM)
- 退出码控制 — 0 成功,2 阻止,其他警告
- 安全第一 — 始终验证输入,审查第三方代码
参考资料
下一节: 2.20 本章小结 — 回顾 Commands、Skills、Plugins、Subagent 和 Hooks 的完整知识体系