# Claude Code Hooks 指南：自动化你的 AI 编程工作流


> 九种 Hook 类型 + 十个秒用场景，把 Claude Code 从工具变成搭档。

---

## 前言

之前写了[《Claude Code 插件完全指南》](/posts/claude-code-plugins-guide/)，介绍四个插件和一个 MCP 服务。那是"用什么"的问题。

本文解决"怎么自动化"的问题：**Hooks 系统**。

Hooks 是 Claude Code 的事件驱动脚本机制——在工具调用、会话启停、权限请求等节点执行自定义逻辑。如果你用过 Git hooks 或 GitHub Actions，概念完全一样。

读完你会知道：九种 Hook 分别干什么，怎么配，以及十个让你想立刻动手的妙用场景。

---

## 基础：Hook 怎么配

所有 Hook 配置在 `.claude/settings.json`（项目级）或 `~/.claude/settings.json`（用户级）的 `hooks` 字段下。

结构长这样：

```json
{
  "hooks": {
    "HookType": [
      {
        "matcher": "Tool1|Tool2",
        "hooks": [
          {
            "type": "command",
            "command": "python3 /path/to/your_script.py"
          }
        ]
      }
    ]
  }
}
```

`matcher` 决定拦截哪些工具调用（`Bash|Write|Edit` 表示只对这三个工具生效）。设为空字符串表示匹配所有。Hook 脚本从 stdin 读 JSON 获取上下文信息，exit 0 表示放行，exit 2 表示阻止（仅部分 Hook 类型支持）。

---

## 九种 Hook 类型

### SessionStart — 会话启动时

最早执行的 Hook。caveman 插件就是靠它注入 caveman 模式提示词。

```json
{
  "matcher": "",
  "hooks": [{
    "type": "command",
    "command": "bash /path/to/startup.sh"
  }]
}
```

传入的上下文：`session_id`、`cwd`、`model` 等。可以用来初始化环境变量、检查依赖、注入提示词。

### UserPromptSubmit — 用户提交消息时

每次你按回车发送消息，这个 Hook 触发。可以在消息进入 Claude 之前附加上下文。

```json
{
  "matcher": "",
  "hooks": [{
    "type": "command",
    "command": "python3 /path/to/inject_context.py"
  }]
}
```

传入信息包含用户的原始消息内容。脚本可以通过 stdout 返回 `hookSpecificOutput` 注入额外文本。

### PreToolUse — 工具调用前

最实用的 Hook 之一。可以在工具执行前拦截、修改或阻止调用。

```json
{
  "matcher": "Bash",
  "hooks": [{
    "type": "command",
    "command": "python3 /path/to/pre_check.py"
  }]
}
```

传入 `tool_name`、`tool_input`。exit 2 阻止操作，exit 0 放行。

### PostToolUse — 工具调用后

工具执行完立即触发。传入完整的执行结果，包括 `duration_ms`、`stdout`、`exit_code` 等。

```json
{
  "matcher": "Bash|Write|Edit",
  "hooks": [{
    "type": "command",
    "command": "python3 /path/to/post_process.py"
  }]
}
```

这是实现"长时间任务通知"的核心 Hook。

### PermissionRequest — 权限请求时

Claude Code 弹出权限对话框之前触发。xiaozhi MCP 用它来记录权限请求日志。

```json
{
  "matcher": "Bash|Write|Edit",
  "hooks": [{
    "type": "command",
    "command": "python3 /path/to/perm_hook.py"
  }]
}
```

可以用来实现自定义的权限策略：白名单自动放行、危险操作二次确认。

### Notification — 收到通知时

外部系统可以通过通知机制向 Claude Code 发送消息。配合 cron 或 CI webhook 使用效果最佳。

### Stop — 响应结束时

Claude 回复完毕、进入等待状态时触发。适合做清理、保存状态、更新状态栏。

### SubagentStop — 子代理结束时

使用 Agent 工具启动的子代理完成时触发。

### PreCompact — 上下文压缩前

上下文接近窗口上限、触发自动压缩前执行。可以在这里抢救关键信息，防止压缩丢失上下文。

---

## 十个秒用场景

### 场景一：自动格式化 + 刷新

写完代码 → 自动格式化 → 自动刷新浏览器。一步不落。

```python
# post_tool_hook.py — PostToolUse，matcher: Write|Edit
import json, sys, subprocess, os

data = json.loads(sys.stdin.read())
file_path = data.get("tool_input", {}).get("file_path", "")

if file_path.endswith((".py", ".ts", ".tsx", ".json")):
    subprocess.run(["npx", "prettier", "--write", file_path], timeout=10)

# Hugo 博客：markdown 变更时刷新浏览器
if file_path.endswith(".md"):
    subprocess.run(["touch", ".hugo_build_trigger"])  # 触发 air 重载

sys.exit(0)
```

配一次，之后写代码再也不用想格式化的事。

### 场景二：危险命令拦截

防呆不防傻。PreToolUse 在 Bash 执行前拦截高危操作。

```python
# pre_tool_guard.py — PreToolUse，matcher: Bash
import json, sys

data = json.loads(sys.stdin.read())
cmd = data.get("tool_input", {}).get("command", "")

DANGER = [
    "rm -rf /",
    "git push --force main",
    "git push --force master",
    "DROP TABLE",
    "DROP DATABASE",
    "> /dev/sda",
]

for pattern in DANGER:
    if pattern in cmd:
        # 向 Claude 返回阻止信息
        print(json.dumps({
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "deny",
                "permissionDecisionReason": f"危险命令已拦截: {pattern}"
            }
        }))
        sys.exit(2)

sys.exit(0)
```

不影响正常工作流，只在真正危险时亮红灯。

### 场景三：长时间任务飞书通知

Bash 跑了超过 5 分钟 → 飞书机器人推消息到你手机上。不需要盯着终端。

```python
# long_task_notify.py — PostToolUse，matcher: Bash
import json, sys, os, threading

THRESHOLD = int(os.getenv("NOTIFY_THRESHOLD_SEC", "300"))
WEBHOOK = os.getenv("FEISHU_WEBHOOK", "")

data = json.loads(sys.stdin.read())
duration_s = data.get("duration_ms", 0) / 1000

if duration_s < THRESHOLD or not WEBHOOK:
    sys.exit(0)

tool = data.get("tool_name", "")
cmd = data.get("tool_input", {}).get("command", "")[:80]

def send():
    try:
        import requests
        requests.post(WEBHOOK, json={
            "msg_type": "text",
            "content": {"text": f"⏰ Claude Code 任务完成\n工具: {tool}\n耗时: {duration_s:.0f}s\n命令: {cmd}"}
        }, timeout=5)
    except Exception:
        pass

threading.Thread(target=send, daemon=True).start()
sys.exit(0)
```

关键点：daemon 线程 + timeout=5，主进程立刻退出，HTTP 调用不阻塞 Claude Code。

### 场景四：空闲超时飞书通知

有时候不是你盯着 Claude Code 等它，而是它在等你——你切出去干别的，忘了回来。空闲超过 5 分钟，飞书推你。

这个场景需要 Stop Hook + 后台检测器配合：Stop 记录最后活跃时间，同时启动一个延迟检查器，5 分钟后如果时间戳没更新（没有新的 Stop 事件），说明你离开了。

```python
# idle_notify.py — Stop hook，matcher: ""
import json, sys, os, time, subprocess

TIMESTAMP_FILE = "/tmp/claude-last-stop"
THRESHOLD = int(os.getenv("IDLE_THRESHOLD_SEC", "300"))
WEBHOOK = os.getenv("FEISHU_WEBHOOK", "")

now = time.time()
with open(TIMESTAMP_FILE, "w") as f:
    f.write(str(now))

if not WEBHOOK:
    sys.exit(0)

# 后台检测脚本：睡 threshold 秒后检查时间戳是否未变
checker = f'''
import time, requests
webhook = {repr(WEBHOOK)}
threshold = {THRESHOLD}
last_active = {now}

time.sleep(threshold)
try:
    with open("{TIMESTAMP_FILE}") as f:
        current = float(f.read().strip())
    if current == last_active:
        idle_min = threshold // 60
        requests.post(webhook, json={{
            "msg_type": "text",
            "content": {{"text": f"🫥 Claude Code 空闲超过 {{idle_min}} 分钟，你还在吗？"}}
        }}, timeout=5)
except Exception:
    pass
'''

subprocess.Popen(
    ["python3", "-c", checker],
    stdout=subprocess.DEVNULL,
    stderr=subprocess.DEVNULL,
    start_new_session=True,   # daemonize，不随 Stop hook 退出
)
sys.exit(0)
```

核心技巧：`start_new_session=True` 让子进程脱离 Stop hook 的生命周期独立运行。每次 Claude 回复完毕触发 Stop，覆盖时间戳并重新启动一个检测器。如果连续 5 分钟没有新回复，上一次的检测器就会发通知。

和场景三配合：场景三通知你"活干完了"，场景四通知你"你人跑了"。

### 场景五：自动注入状态快照

每次发消息，Claude 自动看到当前 git 状态和最近操作日志。不需要你手动 `git status`。

```python
# inject_git_status.py — UserPromptSubmit，matcher: ""
import json, sys, subprocess, os

def run(cmd):
    try:
        return subprocess.check_output(cmd, shell=True, text=True, timeout=5).strip()
    except Exception:
        return ""

git_status = run("git status --short")
git_log = run("git log --oneline -3")

context = ""
if git_status:
    context += f"Git 状态:\n{git_status}\n"
if git_log:
    context += f"最近提交:\n{git_log}"

if context:
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "UserPromptSubmit",
            "additionalContext": context
        }
    }))

sys.exit(0)
```

效果：Claude 始终知道工程的 git 状态，回答更准确，不需要你重复描述上下文。

### 场景六：PreCompact 防上下文丢失

上下文压缩是 Claude Code 的自动机制，但可能丢掉关键任务信息。PreCompact Hook 在压缩前把当前任务状态写到一个恢复文件，压缩后 Claude 可以通过它找回方向。

```python
# save_context.py — PreCompact，matcher: ""
import json, sys, os

data = json.loads(sys.stdin.read())

# 把当前任务摘要写到恢复文件
backup = {
    "session_id": data.get("session_id"),
    "cwd": data.get("cwd"),
    "trigger": "pre_compact",
    "timestamp": data.get("timestamp")
}

with open("/tmp/claude-context-backup.json", "w") as f:
    json.dump(backup, f)

sys.exit(0)
```

配合 SessionStart Hook 读取这个备份文件，可以实现跨压缩的任务连续性。

### 场景七：远程审批推送

Hooks 的长链玩法：不只通知，还能让你在手机上直接审批 Claude Code 的权限请求。

原理不复杂——Hook 只做第一步拦截，后面交给一个服务端处理全链路。

{{< mermaid >}}
sequenceDiagram
    participant H as PermissionRequest Hook
    participant S as 服务端
    participant F as 飞书/App
    participant U as 你（手机）
    participant P as PTY
    participant C as Claude Code

    H->>S: POST /permission-request
    S->>F: 推送审批卡片/通知
    F->>U: 显示 [批准] [拒绝]
    U->>F: 点击按钮
    F->>S: 回调审批结果
    S->>P: 写入 "1" 或 "3"
    P->>C: 按键注入，对话框关闭
{{< /mermaid >}}

换成流程图视角，数据流向是这样的：

{{< mermaid >}}
flowchart TD
    A[PermissionRequest Hook] -->|写文件 / 调接口| B["服务端 Python/Go/Node"]
    B -->|推送审批卡片| C[飞书交互卡片]
    B -->|推送通知| D["自建 App WebSocket"]
    C -->|点击按钮| E["你 手机审批"]
    D -->|点击按钮| E
    E -->|回调审批结果| B
    B -->|注入按键| F[PTY]
    F -->|按键 1 批准 3 拒绝| G["Claude Code 继续 / 中断"]
{{< /mermaid >}}

文字版（ASCII）：

```
PermissionRequest Hook → 写请求到文件或调接口
                            ↓
                    服务端（Python/Go/Node，跑在本机）
                            ↓
                  ┌────────┴────────┐
                  ↓                  ↓
            飞书交互卡片         自建 App (WebSocket)
                  ↓                  ↓
           你在手机上点 [批准] / [拒绝]
                  ↓
           服务端收到回调
                  ↓
         PTY 向终端会话输入 "1" 或 "3"
                  ↓
        Claude Code 收到按键，继续 / 中断
```

Hook 脚本只需把权限请求转交给服务端：

```python
# perm_forward.py — PermissionRequest，matcher: Bash|Write|Edit
import json, sys, requests, os

SERVER = os.getenv("APPROVAL_SERVER", "http://127.0.0.1:9999")
data = json.loads(sys.stdin.read())

# 转交服务端，不等响应
try:
    requests.post(f"{SERVER}/permission-request", json={
        "tool": data.get("tool_name"),
        "hint": data.get("tool_input", {}).get("command", "")[:120],
        "session_id": data.get("session_id"),
    }, timeout=2)
except Exception:
    pass

# Hook 不做审批决策，让 Claude Code 弹原生对话框
# 服务端通过 PTY 在后台替你按键
sys.exit(0)
```

Hook 退出了，但 Claude Code 原生权限弹窗还在等。这时候服务端已经拿到请求数据，推送到了你的手机上——飞书卡片也好，自建 App 的通知栏也好。你点一下按钮，服务端收到回调，通过 PTY 向终端会话注入一个 `1`（批准）或 `3`（拒绝），对话框关闭，Claude Code 继续干活。

飞书方案适合已有飞书工作流的团队，零客户端成本。自建 App 方案更轻量，WebSocket 长连接延迟更低。无论哪种，Hooks 做的都是第一环——**把权限请求从终端里拽出来，丢到你能触达的地方**。

这模式不止用于权限审批。PreToolUse 拦截危险命令可以推，PostToolUse 长时间任务可以推，Stop 空闲提醒已经在场景四推了。把推送逻辑统一交给服务端，每个 Hook 只负责"触发事件"这一件事。

### 场景八：敏感文件保护

场景二拦截的是危险命令。但 Claude 还有一个能力是直接写文件——如果它误改了 `.env`、删了 SSH 密钥，命令拦截管不到。PreToolUse 配合 `Write|Edit` matcher 可以补上这个缺口。

```python
# file_guard.py — PreToolUse，matcher: Write|Edit
import json, sys

data = json.loads(sys.stdin.read())
file_path = data.get("tool_input", {}).get("file_path", "")

PROTECTED = [
    ".env", ".env.local", ".env.production",
    "secrets.yaml", "credentials.json",
    "*.pem", "*.key", "id_rsa",
    "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
]

for pattern in PROTECTED:
    if pattern.replace("*", "") in file_path:
        print(json.dumps({
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "deny",
                "permissionDecisionReason": f"受保护文件，禁止直接修改: {file_path}"
            }
        }))
        sys.exit(2)

sys.exit(0)
```

和场景二一起配，Bash + Write/Edit 两条 PreToolUse 规则，一个管命令一个管文件，安全兜底就完整了。

### 场景九：命令审计日志

PostToolUse 在每次 Bash 执行后追加一条记录到审计日志。出问题时复现、排查误操作、分析 Claude 的命令习惯，全靠这条日志。

```python
# audit_log.py — PostToolUse，matcher: Bash
import json, sys, os
from datetime import datetime

data = json.loads(sys.stdin.read())
cmd = data.get("tool_input", {}).get("command", "")
duration = data.get("duration_ms", 0) / 1000
exit_code = data.get("exit_code", -1)

log_line = (
    f"[{datetime.now().isoformat()}] "
    f"exit={exit_code} "
    f"duration={duration:.1f}s "
    f"cmd={cmd[:200]}\n"
)

log_path = os.path.expanduser("~/.claude/bash-audit.log")
with open(log_path, "a") as f:
    f.write(log_line)

sys.exit(0)
```

跑一阵子后 `cat ~/.claude/bash-audit.log` 就能看到 Claude 都执行了什么、哪些慢、哪些容易失败。安全审计 + 性能诊断两用。

### 场景十：Stop 强制验证——"不跑通不下班"

前面的场景是"做完了通知你"和"你忘了回来提醒你"。这个场景反过来——Claude 想结束响应？先自检：代码改了吗？测试跑了吗？构建通过了吗？

用 Stop hook 的 `prompt` 类型，让 LLM 自己审查本轮对话：

```json
{
  "matcher": "",
  "hooks": [{
    "type": "prompt",
    "prompt": "Review the last assistant response. If any code files were modified or created during this session: 1) Were tests run and did they pass? 2) Did the build succeed? 3) Were all user questions answered? If any check fails, return 'block:<reason>' explaining what's still needed. If all pass, return 'approve'."
  }]
}
```

`prompt` 类型的好处是不需要你写复杂的检测脚本——让 LLM 自己判断。返回 `block` 就阻止 Claude 结束，它会被迫回去修。返回 `approve` 正常结束。

三个检查项可以按项目定制。如果是 Hugo 博客项目，检查改成"文章能不能正常渲染"；如果是 API 服务，改成"接口能不能返回 200"。本质是把"完成标准"写进规则里，让 Claude 自己对照检查。

---

## 插件：Hooks 的打包形态

上面十个场景都是自定义 Hook 脚本。但如果你留意过自己装的插件，会发现很多插件本质上就是打包好的 Hooks。

以你环境里实际跑着的为例：

**caveman 插件** — 两个 Hook 驱动：
- `SessionStart` — 会话启动时注入 caveman 模式提示词，压缩 Claude 输出风格
- `UserPromptSubmit` — 每次提交消息时维持模式不退化，防止多轮对话后 Claude 恢复啰嗦

**hookify 插件** — 自动化规则生成：
- `Stop` hook — 每次响应结束后分析对话，如果发现"这个行为应该被禁止"，自动建议创建新 Hook 规则。Hook 自己进化 Hook，元 Hook。

**superpowers 插件** — 多 Hook 工作流编排：
- `SessionStart` → 加载 TDD、debugging、brainstorming 等开发方法论的提示词
- `UserPromptSubmit` → 检测用户意图，自动匹配对应 Skill 并加载
- `Stop` → 完成检查点验证，确保开发流程不跳步

你会发现这些插件做的事情和前面十个场景本质上一样——Hook 拦截事件，注入逻辑。区别只在于插件把这些 Hook + Skills + 提示词打包好，你可以开箱即用；而自定义 Hook 脚本是给你最灵活的底层能力，配一次想怎么玩都行。

选插件还是自己写？简单法则：**重复出现的通用流程 → 找插件。一次性特定需求 → 自己写 Hook。两者不互斥——插件管大流程，自定义 Hook 补边角。**

---

## 组合拳：推荐配置

从十种场景里挑最值的六条，这是我的推荐：

1. **PreToolUse** — 危险命令拦截 + 敏感文件保护（场景二 + 八，合并一条规则）
2. **PostToolUse** — 自动格式化 + 审计日志（场景一 + 九）
3. **PostToolUse** — 长任务飞书通知（场景三，改 Slack/钉钉同理）
4. **Stop** — 空闲超时飞书通知（场景四）
5. **UserPromptSubmit** — git 状态注入（场景五）
6. **Stop** — 强制验证（场景十，宽松起步）

远程审批（场景七）需要额外服务端，PreCompact（场景六）按需启用，先不放进基础配置。

全部配在 `~/.claude/settings.json`，一次配置，所有项目生效。场景十的 Stop 强制验证可以先从宽松规则开始——只检查"测试跑没跑"，不检查通过率——等 Claude 习惯了再收紧。

---

## 注意事项

Hooks 脚本在 Claude Code 进程内同步执行（除非设置 `parallel: true`）。脚本出错不会影响 Claude Code 运行——exit 非零只是丢弃本次事件。但脚本卡死是可能的，务必加 timeout。

对于网络请求（飞书通知、日志上报），用 daemon 线程 + timeout 避免阻塞。

另外 Hooks 脚本的日志不会显示在 Claude Code 界面里——调试时写文件日志，或直接 `python3 script.py` 在终端单独跑。

---

## 结语

Hooks 本质是把 Claude Code 从"被动的对话工具"变成"可编程的自动化平台"。如果说插件扩展了 Claude 的能力边界，那 Hooks 扩展的是**工作流的自动化边界**。

每个 Hook 脚本不超过 30 行代码，配一次，受益终身。试过飞书通知之后，你就回不去了。

---

