Skip to content

2.8 Hooks:自动化工作流的钩子系统

什么是 Hooks

Hooks 是用户定义的 Shell 命令,在 Claude Code 生命周期的特定时刻自动执行。

与 Slash Commands 和 Skills 不同,Hooks 提供了确定性控制——它们不依赖 LLM 的判断,而是在特定事件发生时必然执行。

核心价值

特性说明
确定性执行不依赖 AI 判断,每次都会执行
自动化无需手动触发,事件驱动
质量保障自动格式化、验证、安全检查
合规审计自动记录所有操作日志
权限控制阻止危险操作,保护敏感文件

与其他扩展方式对比

维度HooksSlash CommandsSkillsSubagent
触发方式自动(事件驱动)手动(/命令自动/手动手动/自动
执行确定性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发送通知时自定义通知方式桌面通知、声音提醒
StopClaude 完成响应时强制继续执行任务完成验证
SubagentStop子代理任务完成时强制继续执行子任务验证
SessionStart会话启动/恢复时注入环境变量环境初始化
SessionEnd会话结束时无法阻止清理、日志保存
PreCompact执行压缩操作前添加自定义指令保留重要上下文

配置文件位置

Hooks 通过 JSON 配置文件设置,支持三个位置:

位置文件路径适用范围是否提交 Git
用户级~/.claude/settings.json所有项目
项目级.claude/settings.json当前项目是(推荐)
本地项目.claude/settings.local.json当前项目

优先级: 本地项目 > 项目级 > 用户级


快速开始

方法一:使用 /hooks 命令(推荐)

bash
# 在 Claude Code 中执行
> /hooks
  1. 选择 Hook 事件(如 PreToolUse
  2. 添加匹配器(如 Bash*
  3. 输入 Hook 命令
  4. 选择保存位置

方法二:直接编辑配置文件

创建或编辑 ~/.claude/settings.json

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' >> ~/.claude/bash-commands.log"
          }
        ]
      }
    ]
  }
}

配置语法详解

基本结构

json
{
  "hooks": {
    "事件名称": [
      {
        "matcher": "匹配模式",
        "hooks": [
          {
            "type": "command",
            "command": "要执行的命令",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

匹配器(Matcher)语法

模式说明示例
精确匹配匹配特定工具"Write"
正则表达式匹配多个工具"Edit|Write"
通配符匹配所有"*"""
MCP 工具匹配 MCP 服务器工具"mcp__memory__.*"

Hook 类型

1. Command 类型(Shell 命令)

json
{
  "type": "command",
  "command": "your-shell-command",
  "timeout": 60
}

2. Prompt 类型(LLM 评估)

仅适用于特定事件:StopSubagentStopUserPromptSubmitPreToolUsePermissionRequest

json
{
  "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 格式数据:

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 结构

json
{
  "decision": "block",
  "reason": "说明原因",
  "continue": false,
  "stopReason": "显示给用户的消息",
  "suppressOutput": true,
  "systemMessage": "警告信息",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "拒绝原因"
  }
}

实战案例

案例 1:Bash 命令日志记录

用途: 记录所有执行的 Bash 命令用于审计

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '\"[\" + (now | todate) + \"] \" + .tool_input.command' >> ~/.claude/bash-audit.log"
          }
        ]
      }
    ]
  }
}

案例 2:自动代码格式化

用途: 文件修改后自动运行 Prettier

json
{
  "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、密钥文件等)

json
{
  "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 版本:

json
{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code 等待您的输入\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

Linux 版本:

json
{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "notify-send 'Claude Code' '等待您的输入'"
          }
        ]
      }
    ]
  }
}

案例 5:Bash 命令验证器

用途: 验证 Bash 命令,推荐使用更好的工具

创建脚本 .claude/hooks/bash_validator.py

python
#!/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)

配置文件:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/bash_validator.py\""
          }
        ]
      }
    ]
  }
}

案例 6:用户输入敏感信息检测

用途: 阻止用户在提示中包含敏感信息

创建脚本 .claude/hooks/sensitive_detector.py

python
#!/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)  # 出错时不阻止用户输入

配置文件:

json
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/sensitive_detector.py\""
          }
        ]
      }
    ]
  }
}

案例 7:Markdown 自动格式化

用途: 自动为 Markdown 代码块添加语言标签

创建脚本 .claude/hooks/markdown_formatter.py

python
#!/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:会话环境初始化

用途: 会话开始时自动设置环境变量

json
{
  "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

python
#!/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()

配置文件:

json
{
  "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__<服务器名>__<工具名>

示例配置

json
{
  "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 代码

安全示例:输入验证

python
#!/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)

调试技巧

启用详细输出

bash
# 使用 --debug 标志
claude --debug

# 或在运行时按 Ctrl+O 切换详细模式

验证配置

bash
# 查看当前 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.sh

hooks.json 示例:

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

bash
# 提取文件路径
jq -r '.tool_input.file_path'

# 提取命令
jq -r '.tool_input.command'

# 添加时间戳
jq -r '"[" + (now | todate) + "] " + .tool_input.command'

本节小结

  1. Hooks = 自动化规则引擎 — 确定性执行,不依赖 AI 判断
  2. 10 种事件类型 — 覆盖完整的 Claude Code 生命周期
  3. 三个配置位置 — 用户级、项目级、本地项目级
  4. 两种 Hook 类型 — Command(Shell)和 Prompt(LLM)
  5. 退出码控制 — 0 成功,2 阻止,其他警告
  6. 安全第一 — 始终验证输入,审查第三方代码

参考资料


下一节: 2.20 本章小结 — 回顾 Commands、Skills、Plugins、Subagent 和 Hooks 的完整知识体系

基于 MIT 许可证发布。内容版权归作者所有。