九种 Hook 类型 + 十个秒用场景,把 Claude Code 从工具变成搭档。
前言
之前写了《Claude Code 插件完全指南》,介绍四个插件和一个 MCP 服务。那是"用什么"的问题。
本文解决"怎么自动化"的问题:Hooks 系统。
Hooks 是 Claude Code 的事件驱动脚本机制——在工具调用、会话启停、权限请求等节点执行自定义逻辑。如果你用过 Git hooks 或 GitHub Actions,概念完全一样。
读完你会知道:九种 Hook 分别干什么,怎么配,以及十个让你想立刻动手的妙用场景。
基础:Hook 怎么配
所有 Hook 配置在 .claude/settings.json(项目级)或 ~/.claude/settings.json(用户级)的 hooks 字段下。
结构长这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
{
"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 模式提示词。
1
2
3
4
5
6
7
|
{
"matcher": "",
"hooks": [{
"type": "command",
"command": "bash /path/to/startup.sh"
}]
}
|
传入的上下文:session_id、cwd、model 等。可以用来初始化环境变量、检查依赖、注入提示词。
UserPromptSubmit — 用户提交消息时
每次你按回车发送消息,这个 Hook 触发。可以在消息进入 Claude 之前附加上下文。
1
2
3
4
5
6
7
|
{
"matcher": "",
"hooks": [{
"type": "command",
"command": "python3 /path/to/inject_context.py"
}]
}
|
传入信息包含用户的原始消息内容。脚本可以通过 stdout 返回 hookSpecificOutput 注入额外文本。
最实用的 Hook 之一。可以在工具执行前拦截、修改或阻止调用。
1
2
3
4
5
6
7
|
{
"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 等。
1
2
3
4
5
6
7
|
{
"matcher": "Bash|Write|Edit",
"hooks": [{
"type": "command",
"command": "python3 /path/to/post_process.py"
}]
}
|
这是实现"长时间任务通知"的核心 Hook。
PermissionRequest — 权限请求时
Claude Code 弹出权限对话框之前触发。xiaozhi MCP 用它来记录权限请求日志。
1
2
3
4
5
6
7
|
{
"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 — 上下文压缩前
上下文接近窗口上限、触发自动压缩前执行。可以在这里抢救关键信息,防止压缩丢失上下文。
十个秒用场景
场景一:自动格式化 + 刷新
写完代码 → 自动格式化 → 自动刷新浏览器。一步不落。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# 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 执行前拦截高危操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
# 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 分钟 → 飞书机器人推消息到你手机上。不需要盯着终端。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
# 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 事件),说明你离开了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
# 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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
# 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 可以通过它找回方向。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# 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 只做第一步拦截,后面交给一个服务端处理全链路。
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: 按键注入,对话框关闭
换成流程图视角,数据流向是这样的:
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 继续 / 中断"]
文字版(ASCII):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
PermissionRequest Hook → 写请求到文件或调接口
↓
服务端(Python/Go/Node,跑在本机)
↓
┌────────┴────────┐
↓ ↓
飞书交互卡片 自建 App (WebSocket)
↓ ↓
你在手机上点 [批准] / [拒绝]
↓
服务端收到回调
↓
PTY 向终端会话输入 "1" 或 "3"
↓
Claude Code 收到按键,继续 / 中断
|
Hook 脚本只需把权限请求转交给服务端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# 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 可以补上这个缺口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
# 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 的命令习惯,全靠这条日志。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# 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 自己审查本轮对话:
1
2
3
4
5
6
7
|
{
"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 补边角。
组合拳:推荐配置
从十种场景里挑最值的六条,这是我的推荐:
- PreToolUse — 危险命令拦截 + 敏感文件保护(场景二 + 八,合并一条规则)
- PostToolUse — 自动格式化 + 审计日志(场景一 + 九)
- PostToolUse — 长任务飞书通知(场景三,改 Slack/钉钉同理)
- Stop — 空闲超时飞书通知(场景四)
- UserPromptSubmit — git 状态注入(场景五)
- 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 行代码,配一次,受益终身。试过飞书通知之后,你就回不去了。