<!-- title: Claude Code Hooks深度实战：13个事件全解析 -->

## 概述

本教程深入剖析Claude Code Hooks系统的完整事件模型、JSON输入输出协议、退出码语义和Matcher过滤机制。通过12个生命周期事件的逐一拆解和6个生产级实战案例，帮助开发者构建安全护栏、自动格式化、上下文注入、会话管理等能力。适合已具备Claude Code基础使用经验、希望将开发流程自动化到极致的中高级开发者。

## 前置条件

- Claude Code CLI 已安装且基本使用熟练
- Python 3.8+ 或 Node.js 18+（hook脚本语言任选）
- 了解JSON结构和Shell脚本基础
- 理解正则表达式基础语法
- 一个正在进行的Git项目（用于实战演练）

---

## 第一章：Hooks架构全景——理解事件驱动模型

Claude Code的Hooks系统本质上是一个**事件驱动的中间件层**。每当Claude Code执行工具调用、用户提交prompt、会话启动或结束时，都会触发预注册的hook脚本。这些脚本可以**拦截、修改、阻止或记录**每一个操作。

### 事件生命周期总览


![图表1](images/claude-code-tutorial-r2-1856-mermaid-0.png)

> *图表 1*


### 完整事件清单

Claude Code支持13个hook事件，分为三大类：

| 类别 | 事件 | 触发时机 | Matcher支持 |
|------|------|----------|-------------|
| **工具生命周期** | `PreToolUse` | 工具执行前 | 正则匹配tool_name |
| | `PostToolUse` | 工具执行后 | 正则匹配tool_name |
| | `PostToolUseFailure` | 工具执行失败后 | 正则匹配tool_name |
| **会话生命周期** | `SessionStart` | 会话启动/恢复 | 不支持 |
| | `SessionEnd` | 会话结束 | 不支持 |
| | `Setup` | 首次配置 | 不支持 |
| | `Stop` | 主Agent完成回复 | 不支持 |
| | `SubagentStart` | 子Agent启动 | 不支持 |
| | `SubagentStop` | 子Agent完成 | 不支持 |
| **用户交互** | `UserPromptSubmit` | 用户提交prompt | 不支持 |
| | `Notification` | 系统通知（权限请求等） | 不支持 |
| | `PermissionRequest` | 权限请求 | 不支持 |
| **上下文管理** | `PreCompact` | 对话压缩前 | 不支持 |

### 配置文件层级

Hooks可以配置在三个层级，后加载的覆盖先加载的：

```
~/.claude/settings.json          # 全局配置，所有项目生效
项目/.claude/settings.json       # 项目级配置，团队共享
项目/.claude/settings.local.json # 本地配置，不提交到Git
```

---

## 第二章：JSON协议详解——stdin输入与stdout输出

每个hook脚本通过**stdin接收JSON**，通过**stdout返回JSON**，通过**退出码传达决策**。这是整个系统最核心的通信协议。

### 通用输入信封

所有hook事件共享的基础输入结构：

```json
{
  "session_id": "abc123-def456",
  "transcript_path": "/tmp/claude-session-abc123.jsonl",
  "cwd": "/home/user/my-project",
  "hook_event_name": "PreToolUse"
}
```

### 退出码语义

退出码是hook控制Claude Code行为的核心手段：

| 退出码 | 含义 | stdout行为 | stderr行为 |
|--------|------|-----------|-----------|
| **0** | 成功/放行 | 对`UserPromptSubmit`和`SessionStart`注入上下文，其他事件仅记录 | 忽略 |
| **2** | 阻止/拦截 | 忽略 | 路由到Claude或用户（见下表） |
| **其他** | 非阻塞错误 | 忽略 | 显示给用户 |

### Exit Code 2行为矩阵


![图表2](images/claude-code-tutorial-r2-1856-mermaid-1.png)

> *图表 2*


关键区别：**stderr路由目标是Claude还是用户**决定了你的错误消息应该写给谁看。当stderr发给Claude时，Claude会根据消息内容调整后续行为；发给用户时，仅作为显示信息。

---

## 第三章：PreToolUse——安全护栏的第一道防线

`PreToolUse`是使用频率最高的hook事件，它在Claude准备执行工具但**还未执行**时触发。你可以用它来阻止危险命令、保护敏感文件、执行预检查。

### 输入Schema

```json
{
  "session_id": "abc123",
  "transcript_path": "/tmp/session.jsonl",
  "cwd": "/home/user/project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf node_modules",
    "description": "Clean node_modules"
  }
}
```

### 输出选项

**推荐方式（hookSpecificOutput）：**

```json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "不允许删除node_modules，请使用 npm ci 重建"
  }
}
```

三种决策值：
- `"allow"` — 绕过权限UI，直接执行（原因显示给**用户**）
- `"deny"` — 阻止调用（原因显示给**Claude**，Claude会据此调整）
- `"ask"` — 弹出权限确认UI（原因显示给**用户**）

**Legacy方式（仍然支持）：**

```json
{
  "decision": "block",
  "reason": "危险命令已被拦截"
}
```

### Matcher过滤

只有`PreToolUse`和`PostToolUse`支持`matcher`字段，它是一个**正则表达式**，匹配`tool_name`：

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/check_bash.py"
          }
        ]
      },
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/check_file_write.py"
          }
        ]
      }
    ]
  }
}
```

对于MCP工具，名称格式为`mcp__<server>__<tool>`，例如：

```json
{
  "matcher": "mcp__filesystem__write_file",
  "hooks": [...]
}
```

空字符串`""`匹配所有工具。

### 实战案例1：阻止危险Bash命令

创建文件 `.claude/hooks/pre_tool_use.py`：

```python
#!/usr/bin/env python3
"""PreToolUse Hook: 拦截危险的Bash命令和敏感文件访问"""

import json
import sys
import re
from pathlib import Path


def is_dangerous_rm(command: str) -> bool:
    """检测危险的rm命令模式"""
    normalized = ' '.join(command.lower().split())
    
    patterns = [
        r'\brm\s+.*-[a-z]*r[a-z]*f',     # rm -rf, rm -fr
        r'\brm\s+--recursive\s+--force',   # 长选项格式
        r'\brm\s+-r\s+.*-f',               # 分开的选项
    ]
    
    for pattern in patterns:
        if re.search(pattern, normalized):
            return True
    
    # rm -r 针对危险路径
    dangerous_paths = ['/', '/*', '~', '~/', '$HOME', '*', '.']
    if re.search(r'\brm\s+.*-[a-z]*r', normalized):
        for path in dangerous_paths:
            if re.search(re.escape(path), normalized):
                return True
    
    return False


def is_protected_file(tool_name: str, tool_input: dict) -> bool:
    """检查是否访问受保护的文件"""
    protected = ['.env', '.env.production', '.env.staging']
    
    if tool_name in ('Read', 'Edit', 'Write', 'MultiEdit'):
        file_path = tool_input.get('file_path', '')
        for p in protected:
            if p in file_path and not file_path.endswith('.env.sample'):
                return True
    
    return False


def main():
    try:
        data = json.load(sys.stdin)
        tool_name = data.get('tool_name', '')
        tool_input = data.get('tool_input', {})
        
        # 检查受保护文件
        if is_protected_file(tool_name, tool_input):
            msg = json.dumps({
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason":
                        "受保护的.env文件不可修改，请使用.env.sample作为模板"
                }
            })
            print(msg)
            sys.exit(0)
        
        # 检查危险Bash命令
        if tool_name == 'Bash':
            command = tool_input.get('command', '')
            if is_dangerous_rm(command):
                msg = json.dumps({
                    "hookSpecificOutput": {
                        "hookEventName": "PreToolUse",
                        "permissionDecision": "deny",
                        "permissionDecisionReason":
                            f"检测到危险删除命令: '{command[:80]}'。已被安全策略拦截。"
                    }
                })
                print(msg)
                sys.exit(0)
        
        sys.exit(0)  # 放行
        
    except Exception:
        sys.exit(0)  # 异常时放行，不阻塞工作流


if __name__ == '__main__':
    main()
```

在`.claude/settings.json`中注册：

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/pre_tool_use.py"
          }
        ]
      }
    ]
  }
}
```

**预期效果：** 当Claude尝试执行`rm -rf /`或编辑`.env`文件时，命令会被阻止，Claude会收到原因说明并调整行为。

---

## 第四章：PostToolUse——自动格式化与结果验证

`PostToolUse`在工具**成功执行后**触发。它无法阻止已发生的操作，但可以：
- 对工具结果给出自动反馈
- 触发后续自动化（格式化、lint、测试）
- 记录审计日志

### 输入Schema

```json
{
  "session_id": "abc123",
  "hook_event_name": "PostToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/home/user/project/src/app.tsx",
    "content": "export default function App() { return <div>Hello</div> }"
  },
  "tool_response": {
    "success": true
  }
}
```

### 实战案例2：自动格式化写入的代码

```python
#!/usr/bin/env python3
"""PostToolUse Hook: 自动格式化编辑过的文件"""

import json
import sys
import subprocess
from pathlib import Path


# 需要自动格式化的文件扩展名和对应命令
FORMATTERS = {
    '.ts': 'npx prettier --write {path}',
    '.tsx': 'npx prettier --write {path}',
    '.js': 'npx prettier --write {path}',
    '.jsx': 'npx prettier --write {path}',
    '.css': 'npx prettier --write {path}',
    '.json': 'npx prettier --write {path}',
    '.py': 'python3 -m black {path}',
    '.go': 'gofmt -w {path}',
    '.rs': 'rustfmt {path}',
}


def should_format(tool_name: str, tool_input: dict) -> str | None:
    """判断是否需要格式化，返回文件路径或None"""
    if tool_name not in ('Write', 'Edit', 'MultiEdit'):
        return None
    
    file_path = tool_input.get('file_path', '')
    suffix = Path(file_path).suffix
    
    if suffix in FORMATTERS:
        return file_path
    return None


def main():
    try:
        data = json.load(sys.stdin)
        tool_name = data.get('tool_name', '')
        tool_input = data.get('tool_input', {})
        
        file_path = should_format(tool_name, tool_input)
        if not file_path:
            sys.exit(0)
        
        suffix = Path(file_path).suffix
        cmd = FORMATTERS[suffix].format(path=file_path)
        
        result = subprocess.run(
            cmd, shell=True, capture_output=True, text=True, timeout=30
        )
        
        if result.returncode == 0:
            # 格式化成功，给Claude一个反馈
            output = json.dumps({
                "decision": None,
                "reason": f"已自动格式化 {file_path}"
            })
            print(output)
        else:
            # 格式化失败，让Claude知道
            output = json.dumps({
                "decision": "block",
                "reason": f"格式化失败: {result.stderr[:200]}"
            })
            print(output)
        
        sys.exit(0)
        
    except Exception as e:
        print(f"格式化hook异常: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()
```

配置Matcher精确匹配文件修改工具：

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/auto_format.py"
          }
        ]
      }
    ]
  }
}
```

---

## 第五章：UserPromptSubmit——上下文注入与Prompt预处理

`UserPromptSubmit`在用户按下Enter提交prompt**之前**触发。它有两个独特能力：
1. **阻止prompt**：可以拦截不合适的输入
2. **注入上下文**：通过stdout向Claude的上下文中追加额外信息

### 输入Schema

```json
{
  "session_id": "abc123",
  "hook_event_name": "UserPromptSubmit",
  "prompt": "帮我删除所有node_modules文件夹"
}
```

### 上下文注入机制

这是`UserPromptSubmit`最强大的特性。当hook以exit code 0退出时，**stdout内容会被注入到Claude的上下文中**，相当于用户在prompt后面附加了额外信息，但用户自己看不到。

### 实战案例3：自动附加Git上下文

```python
#!/usr/bin/env python3
"""UserPromptSubmit Hook: 自动注入当前分支和最近改动信息"""

import json
import sys
import subprocess


def get_git_context() -> str:
    """获取Git仓库当前状态"""
    parts = []
    
    # 当前分支
    result = subprocess.run(
        ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
        capture_output=True, text=True, timeout=5
    )
    if result.returncode == 0:
        branch = result.stdout.strip()
        parts.append(f"当前Git分支: {branch}")
    
    # 未提交的改动数量
    result = subprocess.run(
        ['git', 'status', '--porcelain'],
        capture_output=True, text=True, timeout=5
    )
    if result.returncode == 0:
        changes = [l for l in result.stdout.strip().split('\n') if l]
        if changes:
            parts.append(f"未提交的文件改动: {len(changes)}个")
            # 列出前5个改动文件
            for change in changes[:5]:
                parts.append(f"  {change}")
    
    # 最近3条commit
    result = subprocess.run(
        ['git', 'log', '--oneline', '-3'],
        capture_output=True, text=True, timeout=5
    )
    if result.returncode == 0 and result.stdout.strip():
        parts.append("最近提交:")
        for line in result.stdout.strip().split('\n'):
            parts.append(f"  {line}")
    
    return '\n'.join(parts) if parts else ""


def main():
    try:
        data = json.load(sys.stdin)
        prompt = data.get('prompt', '')
        
        # 检测到关键词时注入额外上下文
        keywords = ['提交', 'commit', '发布', 'deploy', 'push', '测试', 'test']
        should_inject = any(kw in prompt.lower() for kw in keywords)
        
        if should_inject:
            context = get_git_context()
            if context:
                # stdout内容会被注入到上下文
                print(f"\n[自动上下文]\n{context}\n")
        
        sys.exit(0)
        
    except Exception:
        sys.exit(0)


if __name__ == '__main__':
    main()
```

---

## 第六章：SessionStart——会话引导与环境初始化

`SessionStart`在每次启动或恢复Claude Code会话时触发。与`UserPromptSubmit`类似，它的stdout（exit code 0时）会**注入到上下文中**，这使它成为会话引导的绝佳工具。

### 输入Schema

```json
{
  "session_id": "abc123",
  "hook_event_name": "SessionStart",
  "source": "startup",   // "startup" | "resume" | "clear"
  "cwd": "/home/user/project"
}
```

### 实战案例4：根据项目类型注入引导上下文

```python
#!/usr/bin/env python3
"""SessionStart Hook: 根据项目结构自动注入引导信息"""

import json
import sys
from pathlib import Path


def detect_project_type(cwd: str) -> dict:
    """检测项目类型和关键信息"""
    root = Path(cwd)
    info = {"type": "unknown", "tips": []}
    
    # Node.js / React 项目
    if (root / 'package.json').exists():
        info["type"] = "node"
        pkg = json.loads((root / 'package.json').read_text())
        info["name"] = pkg.get('name', 'unknown')
        
        scripts = pkg.get('scripts', {})
        if 'test' in scripts:
            info["tips"].append(f"测试命令: npm test ({scripts['test']})")
        if 'build' in scripts:
            info["tips"].append(f"构建命令: npm run build ({scripts['build']})")
        if 'lint' in scripts:
            info["tips"].append(f"代码检查: npm run lint ({scripts['lint']})")
        
        deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
        if 'react' in deps:
            info["framework"] = f"React {deps['react']}"
        if 'next' in deps:
            info["framework"] = f"Next.js {deps['next']}"
        if 'vue' in deps:
            info["framework"] = f"Vue {deps['vue']}"
    
    # Python 项目
    elif (root / 'pyproject.toml').exists() or (root / 'setup.py').exists():
        info["type"] = "python"
        if (root / 'pyproject.toml').exists():
            info["tips"].append("使用 uv 管理依赖")
        if (root / 'pytest.ini').exists() or (root / 'conftest.py').exists():
            info["tips"].append("测试命令: pytest")
    
    # Go 项目
    elif (root / 'go.mod').exists():
        info["type"] = "go"
        info["tips"].extend([
            "构建: go build ./...",
            "测试: go test ./...",
            "格式化: gofmt -w ."
        ])
    
    # Rust 项目
    elif (root / 'Cargo.toml').exists():
        info["type"] = "rust"
        info["tips"].extend([
            "构建: cargo build",
            "测试: cargo test",
            "格式化: cargo fmt"
        ])
    
    # 检测CLAUDE.md
    if (root / 'CLAUDE.md').exists():
        info["tips"].append("已检测到CLAUDE.md项目配置文件")
    
    return info


def main():
    try:
        data = json.load(sys.stdin)
        cwd = data.get('cwd', '.')
        source = data.get('source', 'startup')
        
        if source == 'startup':
            info = detect_project_type(cwd)
            
            lines = ["[项目自动识别]"]
            lines.append(f"项目类型: {info['type']}")
            
            if 'framework' in info:
                lines.append(f"框架: {info['framework']}")
            if 'name' in info:
                lines.append(f"项目名: {info['name']}")
            
            if info['tips']:
                lines.append("可用命令:")
                for tip in info['tips']:
                    lines.append(f"  - {tip}")
            
            print('\n'.join(lines))
        
        sys.exit(0)
        
    except Exception:
        sys.exit(0)


if __name__ == '__main__':
    main()
```

**效果：** 每次启动Claude Code时，Claude会自动"看到"项目的技术栈和可用命令，无需手动在CLAUDE.md中重复配置。

---

## 第七章：Stop与SubagentStop——防止过早结束

`Stop`事件在主Agent完成回复时触发，`SubagentStop`在子Agent（通过`Task`工具派发）完成时触发。通过返回`exit code 2`或`decision: "block"`，你可以**阻止Agent停止**，让它继续工作。

### 输入Schema

```json
{
  "session_id": "abc123",
  "hook_event_name": "Stop",
  "stop_hook_active": false
}
```

### 实战案例5：确保代码通过所有检查后才能停止

```python
#!/usr/bin/env python3
"""Stop Hook: 确保所有改动通过lint和测试后才允许结束"""

import json
import sys
import subprocess
from pathlib import Path


def has_uncommitted_changes() -> bool:
    result = subprocess.run(
        ['git', 'diff', '--exit-code'],
        capture_output=True, timeout=10
    )
    return result.returncode != 0


def run_lint() -> tuple[bool, str]:
    """运行lint检查"""
    root = Path.cwd()
    
    if (root / 'package.json').exists():
        result = subprocess.run(
            ['npx', 'eslint', '.', '--max-warnings=0'],
            capture_output=True, text=True, timeout=60
        )
        if result.returncode != 0:
            return False, result.stdout + result.stderr
    
    if (root / 'pyproject.toml').exists():
        result = subprocess.run(
            ['python3', '-m', 'ruff', 'check', '.'],
            capture_output=True, text=True, timeout=60
        )
        if result.returncode != 0:
            return False, result.stdout + result.stderr
    
    return True, ""


def run_tests() -> tuple[bool, str]:
    """运行测试"""
    root = Path.cwd()
    
    if (root / 'package.json').exists():
        result = subprocess.run(
            ['npm', 'test'], capture_output=True, text=True, timeout=120
        )
        if result.returncode != 0:
            return False, result.stdout[-500:]  # 最后500字符
    
    return True, ""


def main():
    try:
        data = json.load(sys.stdin)
        
        if not has_uncommitted_changes():
            sys.exit(0)  # 没有改动，直接放行
        
        # 有未提交改动，执行检查
        lint_ok, lint_err = run_lint()
        if not lint_ok:
            output = json.dumps({
                "decision": "block",
                "reason": f"代码Lint检查未通过，请修复后再完成:\n{lint_err[:500]}"
            })
            print(output)
            sys.exit(0)
        
        test_ok, test_err = run_tests()
        if not test_ok:
            output = json.dumps({
                "decision": "block",
                "reason": f"测试未通过，请修复后再完成:\n{test_err[:500]}"
            })
            print(output)
            sys.exit(0)
        
        sys.exit(0)  # 全部通过
        
    except Exception:
        sys.exit(0)


if __name__ == '__main__':
    main()
```

---

## 第八章：Notification与PreCompact——通知与上下文保护

### Notification事件

在系统通知时触发（如等待权限确认、长时间无输入等）。一个常见用途是**语音提醒**或**桌面通知**。

```python
#!/usr/bin/env python3
"""Notification Hook: 发送桌面通知"""

import json
import sys
import subprocess
import os


def send_desktop_notification(message: str):
    """发送桌面通知（Linux/macOS）"""
    if sys.platform == 'darwin':
        # macOS: 使用osascript
        script = f'display notification "{message}" with title "Claude Code"'
        subprocess.run(['osascript', '-e', script], timeout=5)
    else:
        # Linux: 使用notify-send
        subprocess.run(
            ['notify-send', 'Claude Code', message, '-t', '5000'],
            timeout=5
        )


def main():
    try:
        data = json.load(sys.stdin)
        message = data.get('message', '需要你的关注')
        send_desktop_notification(message)
        sys.exit(0)
    except Exception:
        sys.exit(0)


if __name__ == '__main__':
    main()
```

### PreCompact事件

在对话压缩前触发（手动`/compact`或自动压缩）。可以用来**备份对话记录**或**保存关键上下文**。

```python
#!/usr/bin/env python3
"""PreCompact Hook: 备份压缩前的对话记录"""

import json
import sys
import shutil
from pathlib import Path
from datetime import datetime


def main():
    try:
        data = json.load(sys.stdin)
        transcript_path = data.get('transcript_path', '')
        trigger = data.get('trigger', 'unknown')
        
        if transcript_path and Path(transcript_path).exists():
            # 创建备份目录
            backup_dir = Path('logs') / 'transcript_backups'
            backup_dir.mkdir(parents=True, exist_ok=True)
            
            # 生成带时间戳的备份文件名
            ts = datetime.now().strftime('%Y%m%d_%H%M%S')
            session = Path(transcript_path).stem
            backup = backup_dir / f"{session}_pre_compact_{trigger}_{ts}.jsonl"
            
            shutil.copy2(transcript_path, backup)
        
        sys.exit(0)
    except Exception:
        sys.exit(0)


if __name__ == '__main__':
    main()
```

---

## 第九章：实战项目——构建完整的安全与自动化Hooks套件

将前面所有案例整合为一个完整的项目级配置。

### 项目结构

```
my-project/
├── .claude/
│   ├── settings.json          # Hooks注册配置
│   ├── hooks/
│   │   ├── pre_tool_use.py    # 安全护栏
│   │   ├── post_tool_use.py   # 自动格式化
│   │   ├── user_prompt.py     # Prompt预处理
│   │   ├── session_start.py   # 会话引导
│   │   ├── stop.py            # 完成检查
│   │   ├── notification.py    # 桌面通知
│   │   └── pre_compact.py     # 上下文备份
│   └── data/
│       └── sessions/          # 会话数据存储
├── logs/                      # 审计日志（gitignore）
│   └── .gitkeep
└── .gitignore
```

### 完整settings.json

```json
{
  "permissions": {
    "allow": [
      "Bash(npm:*)",
      "Bash(git:*)",
      "Bash(npx:*)",
      "Bash(python3:*)",
      "Bash(pytest:*)",
      "Read",
      "Glob",
      "Grep"
    ],
    "deny": []
  },
  "hooks": {
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/session_start.py"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash|Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/pre_tool_use.py"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/post_tool_use.py"
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/user_prompt.py"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/stop.py"
          }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/notification.py"
          }
        ]
      }
    ],
    "PreCompact": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/pre_compact.py"
          }
        ]
      }
    ]
  }
}
```

### 完整工作流


![图表3](images/claude-code-tutorial-r2-1856-mermaid-2.png)

> *图表 3*


---

## 第十章：调试技巧与常见问题

### 调试Hook执行

当hook行为不符合预期时，逐步排查：

**1. 检查hook是否被加载**

```bash
# 查看生效的settings配置
cat .claude/settings.json | python3 -m json.tool
```

**2. 手动测试hook脚本**

```bash
# 模拟PreToolUse输入
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | \
  python3 .claude/hooks/pre_tool_use.py
echo "Exit code: $?"
```

**3. 添加日志输出**

在hook脚本中添加日志记录：

```python
import datetime

def log(msg: str):
    with open('logs/hooks_debug.log', 'a') as f:
        ts = datetime.datetime.now().isoformat()
        f.write(f"[{ts}] {msg}\n")
```

**4. 理解stderr路由**

关键规则：
- `PreToolUse` exit 2 → stderr给**Claude**（Claude会读并调整）
- `UserPromptSubmit` exit 2 → stderr给**用户**（Claude看不到）
- `Stop` exit 2 → stderr给**Claude**（Claude继续工作）

### 常见陷阱

| 问题 | 原因 | 解决方案 |
|------|------|----------|
| Hook没有触发 | Matcher写错了 | 检查tool_name大小写，用空字符串测试 |
| stdout没注入上下文 | 只有`UserPromptSubmit`和`SessionStart`支持 | 用`hookSpecificOutput.additionalContext` |
| 阻止了但Claude没看到原因 | stderr路由到了用户而非Claude | 检查事件类型，用JSON输出代替stderr |
| Hook超时 | 脚本执行时间过长 | 给subprocess加timeout，hook总时限约30秒 |
| Python模块缺失 | 使用了第三方库 | 用`uv run --script`加inline依赖声明 |

### 使用uv管理Hook依赖

对于需要第三方库的hook脚本，使用uv的inline script metadata：

```python
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
#     "python-dotenv",
#     "requests",
# ]
# ///

import requests
# ... 正常代码
```

然后在`settings.json`中用`uv run`调用：

```json
{
  "type": "command",
  "command": "uv run .claude/hooks/notification.py"
}
```

---

## 第十一章：高级模式与最佳实践

### 模式1：多Matcher分层策略

```json
{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {"type": "command", "command": "python3 .claude/hooks/check_bash.py"}
      ]
    },
    {
      "matcher": "Write|Edit",
      "hooks": [
        {"type": "command", "command": "python3 .claude/hooks/check_file_write.py"}
      ]
    },
    {
      "matcher": "",
      "hooks": [
        {"type": "command", "command": "python3 .claude/hooks/log_all_tools.py"}
      ]
    }
  ]
}
```

空字符串Matcher匹配所有工具，放在最后作为兜底日志。

### 模式2：环境变量控制Hook行为

```python
#!/usr/bin/env python3
import os

# 通过环境变量控制是否启用某个hook
ENABLE_STRICT_MODE = os.getenv('CLAUDE_HOOK_STRICT', 'false') == 'true'
ENABLE_NOTIFICATIONS = os.getenv('CLAUDE_HOOK_NOTIFY', 'true') == 'true'

if not ENABLE_STRICT_MODE:
    sys.exit(0)  # 跳过检查
```

在`.bashrc`或`.zshrc`中设置：

```bash
export CLAUDE_HOOK_STRICT=true    # 严格模式
export CLAUDE_HOOK_NOTIFY=true    # 开启通知
```

### 模式3：HTTP Hook类型

除了本地命令，hook还支持HTTP端点：

```json
{
  "type": "http",
  "url": "https://your-server.com/api/clause-hook"
}
```

HTTP hook会收到POST请求，body是相同的JSON输入。服务器需要返回对应的JSON输出。这适合：
- 集成企业内部审批系统
- 发送Webhook到Slack/Discord
- 连接CI/CD系统

### 模式4：LLM Hook类型

Hook还可以调用LLM做智能决策：

```json
{
  "type": "llm",
  "prompt": "分析这个Bash命令是否安全。如果涉及rm、curl pipe bash、sudo等危险操作，返回BLOCKED。"
}
```

---

## 第十二章：性能考量

### Hook执行开销

每个hook的执行时间直接影响Claude Code的响应速度。实测数据：

| Hook操作 | 典型耗时 | 优化建议 |
|----------|----------|----------|
| 纯Python判断 | <50ms | 无需优化 |
| Git命令 | 50-200ms | 减少git调用次数 |
| npm/npx命令 | 1-5s | 仅在文件变更时触发 |
| 格式化单个文件 | 200ms-1s | 使用matcher精确匹配 |
| LLM调用 | 2-10s | 仅在关键决策点使用 |

### 优化策略

1. **精确Matcher**：避免空字符串matcher运行重量级hook
2. **快速退出**：在hook脚本开头做快速判断，不需要处理时立即`sys.exit(0)`
3. **缓存机制**：对重复查询的结果做进程内缓存
4. **异步操作**：通知类hook放在`Notification`事件而非`PostToolUse`
5. **条件触发**：通过环境变量或文件标记控制hook是否激活

---

## 延伸阅读

- [Anthropic官方Hooks文档](https://code.claude.com/docs/en/hooks) — 权威的事件、Schema和退出码参考
- [Claude Code Hooks Mastery (GitHub)](https://github.com/disler/claude-code-hooks-mastery) — 完整的生产级hook示例集合，覆盖13个事件
- [Hooks Input & Output Schemas (Gist)](https://gist.github.com/FrancisBourre/50dca37124ecc43eaf08328cdcccdb34) — 实现级JSON Schema参考文档
- [Claude Code Hooks: Events, Matchers & Exit Codes](https://codingnomads.com/claude-code-hooks-events-matchers-exit-codes-52614047) — 事件和Matcher机制的深度解析
- [Automate Workflows with Hooks (官方指南)](https://code.claude.com/docs/en/hooks-guide) — 官方实战指南，包含格式化和安全案例

## 训练营适用性评估

| 维度 | 评估 |
|------|------|
| **适合训练营类型** | AI开发工具训练营、DevOps自动化训练营、企业AI治理训练营 |
| **预计教学时长** | 4-6小时（含动手实验） |
| **难度等级** | 中高级（需要Python/Shell脚本和CLI基础） |
| **实验环境要求** | 本地开发机 + Claude Code CLI + Git项目 |
| **建议分组** | 每人独立完成，最后小组分享各自创建的hook套件 |
| **前置课程** | Claude Code基础使用、CLAUDE.md配置 |

## 参考来源

1. [Hooks Reference - Claude Code Docs](https://code.claude.com/docs/en/hooks) — Anthropic官方hooks事件、配置schema、JSON输入输出格式权威参考
2. [Claude Code Hooks Mastery - GitHub](https://github.com/disler/claude-code-hooks-mastery) — disler维护的完整生产级hooks示例项目，覆盖13个生命周期事件
3. [Claude Code Hooks: Events, Matchers & Exit Codes - CodingNomads](https://codingnomads.com/claude-code-hooks-events-matchers-exit-codes-52614047) — 事件分类、Matcher正则、退出码行为深度解析
4. [Hooks Input & Output Schemas - GitHub Gist](https://gist.github.com/FrancisBourre/50dca37124ecc43eaf08328cdcccdb34) — 实现级JSON Schema完整参考，含所有事件的输入输出结构
5. [Claude Code Hooks: PreToolUse & PostToolUse Tutorial](https://paul-schick.com/posts/claude-code-hooks-pretooluse-posttooluse/) — 以安全防护和自动格式化为核心的实战教程

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
