第 18 章:Hooks 钩子系统
在前三章中,我们分别学习了 MCP 协议、插件系统和 Skills 技能系统——它们都属于"主动调用"型扩展:Claude 需要时才去调用。本章要讲的 Hooks 钩子系统完全不同——它是事件驱动的自动化机制,当特定事件发生时自动执行你预设的逻辑,无需 Claude 主动触发。
Hooks 的概念在任何成熟的工具链中都能找到影子:Git 的 pre-commit / post-commit hooks、Webpack 的插件钩子、React 的生命周期钩子。Claude Code 的 Hooks 继承了同样的设计哲学:在关键生命周期节点,给你注入自定义逻辑的能力。
本章目标:理解 Hooks 的事件驱动模型,掌握 6 种事件类型的用法,能编写和调试自己的 Hooks,建立 Hooks 使用的最佳实践。
18.1 Hooks 是什么
概念类比
如果你写过 Git 项目,一定见过 .git/hooks/ 目录下的这些文件:
pre-commit # 提交前运行(如 lint 检查)
post-commit # 提交后运行(如发送通知)
pre-push # 推送前运行(如跑测试)Claude Code 的 Hooks 本质上是同一件事:在特定事件发生时,自动执行一段你定义的操作。区别在于,Claude Code 的事件不是 Git 操作,而是 Agent 运行时的事件——工具调用、会话状态变化、上下文压缩等。
Hooks 能做什么
有了 Hooks,你可以实现以下自动化场景:
| 场景 | 所用事件 | 效果 |
|---|---|---|
| 代码格式化 | PostToolUse | Claude 编辑文件后自动运行 prettier |
| 操作前备份 | PreToolUse | 修改文件前自动 git stash |
| 安全检查 | PreToolUse | 阻止 rm -rf 等危险命令 |
| 环境加载 | SessionStart | 会话启动时检查 Node.js 版本、加载 .env |
| 状态持久化 | Stop | Claude 停止时保存对话摘要 |
| 自定义通知 | Notification | Claude 完成任务后发送桌面通知 |
配置位置
Hooks 配置在 settings.json 的 hooks 键下。和其他配置一样,支持四个层级:
项目本地配置 (.claude/settings.local.json) → 个人 hooks,不提交 Git
项目配置 (.claude/settings.json) → 团队共享 hooks,提交 Git
用户全局配置 (~/.claude/settings.json) → 个人全局 hooks,所有项目生效{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "/path/to/my-security-check.sh"
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "npx prettier --write ${CLAUDE_TOOL_OUTPUT_FILE}"
}
]
}
}注意:Hooks 和权限规则(allow/deny)是两套不同的系统。权限规则控制"是否允许执行",Hooks 控制"执行前后做什么"。不要在 Hook 中重复实现权限逻辑——权限检查应该交给 allow/deny 规则。
18.2 可用事件类型
Claude Code 提供了 6 种事件类型,覆盖了 Agent 生命周期的主要节点。选择正确的事件是编写有效 Hook 的第一步。
事件全景表
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
| PreToolUse | 工具调用之前 | 阻止危险操作、添加额外检查、操作前备份 |
| PostToolUse | 工具调用之后 | 自动格式化、备份、通知、lint 检查 |
| SessionStart | 会话开始时 | 加载自定义上下文、环境检查、初始化资源 |
| Stop | Claude 停止响应时 | 保存状态、清理资源、记录日志 |
| PreCompact | 压缩上下文前 | 保存关键信息、自定义压缩策略 |
| Notification | 收到通知时 | 自定义通知处理、桌面提醒、日志记录 |
PreToolUse —— 工具调用前
触发条件:Claude Code 准备调用任何工具(Read、Write、Edit、Bash、WebSearch 等)之前触发。
可用数据:
CLAUDE_TOOL_NAME:即将调用的工具名称(如Write、Bash)CLAUDE_TOOL_INPUT:工具调用的完整输入参数(JSON 格式)CLAUDE_SESSION_ID:当前会话 ID
返回值期望:
- 正常执行:Hook 脚本返回 exit code 0,工具调用继续
- 阻止执行:Hook 脚本返回非 0 的 exit code,工具调用被阻止
- 修改参数:Hook 可以修改输入并输出新的参数(高级用法)
常见用例:
- 在执行
rm命令前进行二次确认 - 在写入关键配置文件前自动备份
- 检查网络请求的目标域名是否在白名单中
PostToolUse —— 工具调用后
触发条件:Claude Code 完成工具调用后立即触发。无论工具调用成功还是失败,都会触发。
可用数据:
CLAUDE_TOOL_NAME:已调用的工具名称CLAUDE_TOOL_OUTPUT:工具调用的输出结果CLAUDE_TOOL_OUTPUT_FILE:如果工具输出涉及文件,此为文件路径CLAUDE_TOOL_EXIT_CODE:工具调用的退出码(0 = 成功)CLAUDE_SESSION_ID:当前会话 ID
返回值期望:PostToolUse Hook 的返回值会被忽略(操作已经完成)。主要用于副作用操作。
常见用例:
- 编辑文件后自动运行 prettier / ESLint
- 执行
git commit后自动记录日志 - 文件变更后发送桌面通知
SessionStart —— 会话启动
触发条件:每次新建 Claude Code 会话时触发。恢复已有会话时也会触发。
可用数据:
CLAUDE_SESSION_ID:新会话的 IDCLAUDE_PROJECT_DIR:当前项目根目录
返回值期望:可以输出文本,这些文本会被注入到 Claude 的系统 Prompt 中,作为会话的额外上下文。
常见用例:
- 自动输出当前 Git 分支和最近的提交记录
- 检查 Node.js 版本并警告不兼容
- 加载
.env中的环境变量信息
Stop —— 停止响应
触发条件:Claude 完成当前响应、停止生成时触发。包括正常完成和用户中断两种情况。
可用数据:
CLAUDE_SESSION_ID:当前会话 IDCLAUDE_STOP_REASON:停止原因(complete/interrupted/error)
返回值期望:返回值会被忽略。主要用于清理和通知。
常见用例:
- 将对话摘要保存到文件
- 清理临时文件
- 发送完成通知(桌面通知、Slack 消息等)
PreCompact —— 压缩上下文前
触发条件:当 Claude Code 的上下文窗口即将被占满,需要自动压缩(compact)历史对话时触发。用户手动执行 /compact 时也会触发。
可用数据:
CLAUDE_SESSION_ID:当前会话 IDCLAUDE_COMPACT_TRIGGER:触发原因(auto/manual)
返回值期望:可以输出文本,这些文本会被保留在压缩后的上下文中,不会被丢弃。
常见用例:
- 在压缩前将关键决策、重要信息写入 Memory 文件
- 将当前任务的进展状态保存到文件
- 自定义压缩策略(保留特定类型的消息)
Notification —— 通知处理
触发条件:当 Claude Code 收到系统通知时触发(如权限请求通知、错误通知等)。
可用数据:
CLAUDE_NOTIFICATION_TYPE:通知类型CLAUDE_NOTIFICATION_MESSAGE:通知内容
返回值期望:可以控制通知的显示方式(显示/隐藏/自定义格式)。
常见用例:
- 将权限确认请求转发到移动设备
- 自定义通知格式和显示时长
- 记录所有通知到日志文件
18.3 常用 Hook 场景
本节提供 5 个完整、可直接使用的 Hook 示例。每个示例都包含配置代码和工作原理说明。
场景 1:自动执行代码格式化
目标:Claude 每次编辑或写入文件后,自动运行 prettier 格式化代码。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "npx prettier --write ${CLAUDE_TOOL_OUTPUT_FILE}"
}
]
}
}工作原理:
- Claude 执行
Edit或Write工具修改文件 - 工具完成后,Hook 被触发
matcher: "Edit|Write"确保只有编辑/写入操作才触发,避免 Read、Bash 等其他操作也跑 prettier${CLAUDE_TOOL_OUTPUT_FILE}被替换为被修改文件的绝对路径- prettier 格式化该文件,如果有变更则写回
变体:项目特定的格式化配置
如果你的项目使用 biome 或 eslint 做格式化,只需替换命令:
// 使用 biome
"command": "npx biome format --write ${CLAUDE_TOOL_OUTPUT_FILE}"
// 使用 ESLint fix
"command": "npx eslint --fix ${CLAUDE_TOOL_OUTPUT_FILE}"注意:如果 Claude 一次修改了多个文件,这个 Hook 会对每个文件分别触发一次。不要担心性能——每次只格式一个文件,通常能在毫秒级完成。
场景 2:操作前自动备份
目标:在 Claude 修改文件之前,自动执行 git stash 保存当前状态,以便出问题时恢复。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"command": "git stash push --include-untracked -m 'claude-code-auto-backup: before tool ${CLAUDE_TOOL_NAME}'"
}
]
}
}工作原理:
- Claude 准备调用
Write编辑src/config.ts - PreToolUse Hook 触发,执行
git stash push --include-untracked - 当前工作区的所有变更(包括未追踪文件)被保存到 git stash
- 工具调用继续,Claude 执行编辑
- 如果编辑结果不满意,你可以手动
git stash pop恢复
问题恢复:
# 查看自动备份列表
git stash list
# 输出:
# stash@{0}: On main: claude-code-auto-backup: before tool Write
# stash@{1}: On main: claude-code-auto-backup: before tool Edit
# 恢复到某个备份
git stash pop stash@{0}
# 丢弃所有自动备份
git stash clear # 谨慎使用注意:如果你的工作区本身就是干净的(没有未提交的变更),
git stash push不会创建新的 stash 条目。这是一个安全的操作——不会产生无意义的空 stash。
场景 3:自定义危险命令拦截
目标:在执行特定危险命令前,强制要求额外确认,即使你已经开启了 Bypass 模式。
有两条路可以走。简单版——用 deny 规则直接阻止:
{
"permissions": {
"deny": [
"Bash(rm:-rf:*)",
"Bash(sudo:*)",
"Bash(git:push:--force:*)"
]
}
}deny 规则简单可靠,但它的问题是一刀切:完全阻止,不给任何商量余地。如果你希望的是"先警告、但给我机会放行",那就要用 PreToolUse Hook:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "bash -c '\n CMD=\"$CLAUDE_TOOL_INPUT\"\n if echo \"$CMD\" | grep -qE \"rm -rf|sudo|git push.*--force|DROP TABLE|DELETE FROM\"; then\n echo \"{\\\"decision\\\": \\\"block\\\", \\\"reason\\\": \\\"危险命令检测: $CMD\\\"}\"\n exit 1\n fi\n exit 0\n'"
}
]
}
}工作原理:
- Claude 准备执行任何 Bash 命令
- Hook 脚本解析命令内容,用正则匹配危险模式
- 如果匹配到
rm -rf、sudo、git push --force、DROP TABLE、DELETE FROM等,返回 exit code 1,命令被阻止 - 没有匹配则返回 exit code 0,命令正常执行
可检测的危险模式:
| 模式 | 风险 | regex |
|---|---|---|
rm -rf | 递归强制删除 | rm\s+-rf |
sudo | 提权操作 | sudo |
git push --force | 强制推送覆盖远程 | git\s+push.*--force |
DROP TABLE | 删除数据库表 | DROP\s+TABLE |
DELETE FROM | 删除数据库记录 | DELETE\s+FROM |
chmod 777 | 过度放宽权限 | chmod\s+777 |
你可以根据自己的项目,扩展这个正则表达式列表。
场景 4:会话启动自动加载环境
目标:每次启动新会话时,自动告诉 Claude 当前项目的环境状态——Git 分支、Node 版本、最近的提交等。
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"command": "bash -c '\n echo \"=== 项目环境信息 ===\"\n echo \"Git 分支: $(git branch --show-current)\"\n echo \"Node 版本: $(node -v)\"\n echo \"pnpm 版本: $(pnpm -v)\"\n echo \"\"\n echo \"最近 3 次提交:\"\n git log --oneline -3\n echo \"\"\n echo \"当前变更状态:\"\n git status --short\n'"
}
]
}
}工作原理:
- 每次启动新会话(或恢复已有会话),SessionStart Hook 触发
- Hook 脚本收集项目的 Git 状态、Node 版本等信息
- 脚本的输出被注入到 Claude 的系统 Prompt 中
- Claude 从一开始就知道当前分支、最近的变更、环境版本——无需你再口头描述
效果对比:
没有 Hook 时的对话:
你:帮我修复登录页面的 bug
Claude:(读取大量文件来理解项目状态)好的,我看到你在 main 分支上...有 Hook 时的对话:
(Hook 已自动注入:Git 分支 = fix/login-bug,Node v22.3.0,最近提交 = WIP: login refactor)
你:帮我修复登录页面的 bug
Claude:(已经有了上下文)我看到你在 fix/login-bug 分支上工作,最近的提交是 login refactor。
请描述你遇到的具体 bug?这个差异在长对话中尤其明显——Claude 从第一秒就掌握了环境信息,不需要你每次都解释"我在哪个分支、用的什么版本"。
场景 5:自动 Lint 检查
目标:Claude 编辑代码后,自动运行 ESLint 检查,如果 lint 不通过则让 Claude 修复。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "bash -c '\n FILE=\"$CLAUDE_TOOL_OUTPUT_FILE\"\n if [[ \"$FILE\" =~ \\.(ts|tsx|js|jsx)$ ]]; then\n echo \"Running ESLint on $FILE...\"\n npx eslint --fix \"$FILE\" 2>&1 || echo \"Lint issues remain, Claude will fix in next turn\"\n fi\n'"
}
]
}
}工作原理:
- Claude 编辑
.ts/.tsx/.js/.jsx文件 - PostToolUse Hook 触发
- 脚本检查文件扩展名——只对 JavaScript/TypeScript 文件运行 ESLint
eslint --fix自动修复可修复的规则违规- 如果仍有无法自动修复的 lint 错误,输出信息让 Claude 在下一轮对话中处理
为什么扩展名检查很重要:如果不做扩展名过滤,ESLint 会对 .json、.md、.css 等文件也运行一遍——浪费资源且可能报错。在 Hook 中做过滤是好的实践。
18.4 Hook 编写与调试
Hook 脚本的基本要求
一个有效的 Hook 脚本必须满足:
- 可执行:脚本文件需要有执行权限(Linux/macOS 下
chmod +x),或通过解释器调用(如bash -c '...'、node script.js) - 返回标准的 exit code:0 表示成功,非 0 表示失败(PreToolUse 中非 0 会阻止工具执行)
- 执行速度快:理想情况下 < 1 秒,最慢不应超过 5 秒
- 不依赖用户交互:Hook 在后台运行,无法读取 stdin 或弹出对话框
Hook 命令格式
Hooks 支持两种命令格式:
格式 1:内联 Shell 命令(简单场景)
{
"matcher": "Edit|Write",
"command": "npx prettier --write ${CLAUDE_TOOL_OUTPUT_FILE}"
}格式 2:外部脚本文件(复杂场景)
{
"matcher": "Bash",
"command": "/Users/me/.claude/hooks/security-check.sh"
}推荐:简单逻辑用内联命令,复杂逻辑(超过 5 行、需要错误处理、需要配置读取)写成独立脚本。
可用环境变量
Hooks 运行时,Claude Code 会注入以下环境变量:
| 变量名 | 可用事件 | 说明 |
|---|---|---|
CLAUDE_TOOL_NAME | PreToolUse, PostToolUse | 工具名称,如 Write、Bash、Read |
CLAUDE_TOOL_INPUT | PreToolUse | 工具调用的完整输入参数(JSON 字符串) |
CLAUDE_TOOL_OUTPUT | PostToolUse | 工具调用的输出内容 |
CLAUDE_TOOL_OUTPUT_FILE | PostToolUse | 操作涉及的文件路径(如果适用) |
CLAUDE_TOOL_EXIT_CODE | PostToolUse | 工具调用的退出码 |
CLAUDE_SESSION_ID | 全部事件 | 当前会话的唯一标识符 |
CLAUDE_STOP_REASON | Stop | 停止原因(complete / interrupted / error) |
CLAUDE_COMPACT_TRIGGER | PreCompact | 压缩触发原因(auto / manual) |
CLAUDE_NOTIFICATION_TYPE | Notification | 通知类型 |
CLAUDE_NOTIFICATION_MESSAGE | Notification | 通知内容 |
CLAUDE_PROJECT_DIR | 全部事件 | 当前项目的根目录绝对路径 |
提示:在 Shell 中引用这些变量时,务必加引号:
"${CLAUDE_TOOL_OUTPUT_FILE}"。文件路径可能包含空格。
Hook 失败处理
Hook 执行失败时的行为取决于事件类型:
| 事件 | Hook 失败的行为 |
|---|---|
| PreToolUse | 工具调用被阻止。Claude 收到错误信息,通常会调整策略或询问你 |
| PostToolUse | 工具调用已完成,Hook 失败不影响已完成的操作。错误被记录到日志 |
| SessionStart | 会话正常启动,Hook 输出被丢弃。错误被记录 |
| Stop | 不影响 Claude 的停止行为。错误被记录 |
| PreCompact | 压缩正常进行,Hook 输出的内容不会被保留 |
| Notification | 通知按默认方式处理 |
重要结论:只有 PreToolUse Hook 的失败会直接影响 Claude 的行为。其他事件的 Hook 失败是静默的——不会打断你的工作流,但可能让你失去预期的自动化效果。
调试 Hooks
方法 1:开启 verbose 日志
在 settings.json 中启用调试日志:
{
"env": {
"DEBUG": "claude-code:hooks"
}
}启用后,每次 Hook 的执行过程(触发、参数、返回值、耗时)都会输出到 Claude Code 的日志中。
方法 2:在 Hook 中主动输出日志
在 Hook 脚本中添加 echo 输出,这些输出会出现在 Claude Code 的终端区域:
bash -c '
echo "[Hook: PreToolUse] 检查命令: $CLAUDE_TOOL_INPUT" >> /tmp/claude-hooks.log
echo "[Hook: PreToolUse] 时间: $(date)" >> /tmp/claude-hooks.log
# 实际逻辑...
'方法 3:手动模拟事件
在开发 Hook 时,你不需要真正启动 Claude Code 来测试。手动设置环境变量,直接运行 Hook 脚本:
# 模拟 PostToolUse 事件
CLAUDE_TOOL_NAME="Write" \
CLAUDE_TOOL_OUTPUT_FILE="/tmp/test.ts" \
CLAUDE_SESSION_ID="test-session-001" \
CLAUDE_PROJECT_DIR="/Users/me/my-project" \
bash /path/to/your-hook.sh先确保脚本在独立环境中能正常运行,再配置到 settings.json 中。这能节省大量调试时间。
方法 4:检查 Hook 是否被注册
不确定你的 Hook 配置是否生效?在会话中直接问 Claude:
请列出当前配置的所有 HooksClaude 可以读取 settings.json 并告诉你哪些 Hooks 处于活跃状态。
常见调试问题
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| Hook 没有被触发 | matcher 正则不匹配;配置文件路径不对 | 检查 matcher 是否匹配目标工具名;确认配置文件在正确的层级 |
| Hook 运行但没效果 | 命令执行了但输出被忽略;文件路径不对 | 在命令中加 echo 输出到日志文件;确认环境变量值正确 |
| PreToolUse 总阻止操作 | exit code 非 0;脚本逻辑错误 | 手动运行脚本检查返回值;检查是否误匹配了不该拦截的操作 |
| Hook 太慢 | 命令本身耗时;网络请求阻塞 | 简化 Hook 逻辑;异步启动耗时操作(& 后台执行) |
18.5 Hook 配置语法
matcher 字段:正则过滤
matcher 是一个正则表达式字符串,用于匹配事件相关的工具名或数据。它的作用是过滤——只有匹配的事件才会触发 Hook。
语法要点:
// 匹配单个工具
"matcher": "Write"
// 匹配多个工具:用 | 分隔
"matcher": "Edit|Write"
// 匹配所有工具:留空或使用 ".*"
"matcher": ""
// 匹配 Bash 中的特定命令模式
"matcher": "Bash.*git.*"matcher 匹配规则:
| matcher 值 | 匹配的工具调用 | 不匹配的工具调用 |
|---|---|---|
Write | Write | Edit, Read, Bash |
Edit|Write | Edit, Write | Read, Bash, WebSearch |
"" (空字符串) | 所有工具 | 无 |
Bash | 所有 Bash 调用 | Read, Write, Edit |
最佳实践:matcher 越精确越好。如果所有 PostToolUse 都触发,每次 Read 操作也会跑一次 prettier——完全没必要且浪费资源。
command 字段:要执行的命令
command 可以是任何能在 Shell 中执行的命令:
// 直接命令
"command": "npx prettier --write file.ts"
// 带变量的命令
"command": "npx prettier --write ${CLAUDE_TOOL_OUTPUT_FILE}"
// Shell 脚本片段
"command": "bash -c 'if [ -f \"$CLAUDE_TOOL_OUTPUT_FILE\" ]; then echo \"modified: $CLAUDE_TOOL_OUTPUT_FILE\"; fi'"
// 调用外部脚本
"command": "python3 /path/to/my-hook.py ${CLAUDE_SESSION_ID}"可用变量
在 command 字符串中,你可以使用 ${变量名} 语法引用环境变量:
| 变量 | 替换值 |
|---|---|
${CLAUDE_TOOL_NAME} | Write, Edit, Bash, Read 等 |
${CLAUDE_TOOL_INPUT} | JSON 格式的工具输入参数 |
${CLAUDE_TOOL_OUTPUT} | 工具调用的输出文本 |
${CLAUDE_TOOL_OUTPUT_FILE} | 被操作文件的绝对路径 |
${CLAUDE_SESSION_ID} | 如 session_abc123 |
${CLAUDE_PROJECT_DIR} | 如 /Users/me/my-project |
安全提醒:不要直接在命令中拼接
${CLAUDE_TOOL_INPUT}为 Shell 参数——它可能包含任意内容(包括分号、管道符等 Shell 特殊字符)。需要处理时,通过脚本解析 JSON 而非直接 eval。
同一事件的多个 Hooks:执行顺序
你可以为同一事件配置多个 Hooks:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "npx prettier --write ${CLAUDE_TOOL_OUTPUT_FILE}"
},
{
"matcher": "Edit|Write",
"command": "npx eslint --fix ${CLAUDE_TOOL_OUTPUT_FILE}"
},
{
"matcher": "Edit|Write",
"command": "osascript -e 'display notification \"File modified\"'"
}
]
}
}执行规则:
- 按数组顺序依次执行:prettier 先跑,ESLint 第二,通知最后
- 前一个完成才执行下一个:串行执行,不是并行
- 某个失败不影响后续:prettier 报错不会阻止 ESLint 执行
- PreToolUse 特殊行为:如果某个 PreToolUse Hook 返回非 0,后续 Hook 不再执行,且工具调用被阻止
Hook 超时
Claude Code 对 Hook 执行有默认的超时限制:
| 事件类型 | 默认超时 |
|---|---|
| PreToolUse | 10 秒 |
| PostToolUse | 5 秒 |
| 其他事件 | 15 秒 |
如果 Hook 执行超时:
- PreToolUse:工具调用被允许继续(避免 Hook 阻塞正常工作流)
- 其他事件:Hook 被强制终止,错误记录到日志
如何避免超时:如果你的 Hook 确实需要长时间运行(如大型项目的完整 lint),用后台执行:
{
"matcher": "Edit|Write",
"command": "bash -c 'npx eslint --fix ${CLAUDE_TOOL_OUTPUT_FILE} &' # & 让命令在后台运行"
}后台执行的缺点是 Hook 脚本立即返回 exit 0,你无法知道 lint 是否真的成功了。这是便利性和可靠性之间的权衡。
18.6 Hook 最佳实践
通过大量实践和社区反馈,以下 7 条原则被证明是编写高质量 Hooks 的关键。
1. 快速执行 —— 1 秒以内完成
Hook 是阻塞式的——在 Hook 执行完成之前,Claude 不会进入下一步。如果你的 PreToolUse Hook 需要 5 秒,那么每次工具调用都多等 5 秒,一天累积下来非常可观。
✅ do: npx prettier --write one-file.ts (< 500ms)
❌ don't: npx prettier --write src/**/*.ts (> 10s)
❌ don't: curl https://some-slow-api.com/check (取决于网络)如果必须做耗时操作,使用后台执行(&)或改为异步触发(如写入一个标记文件,由外部 cron 处理)。
2. 幂等性 —— 重复运行不出错
同一个 Hook 可能因为各种原因被多次触发(多个文件操作、配置重叠等)。确保 Hook 多次执行的结果和一次执行一样。
# ❌ 非幂等:每次运行追加一行,重复运行会不断追加
echo "formatted at $(date)" >> /tmp/hook-log.txt
# ✅ 幂等:覆盖写入,不管运行多少次结果都一样
echo "formatted at $(date)" > /tmp/hook-last-run.txt
# ✅ 幂等:检查后再操作
if [ ! -f /tmp/backup-created ]; then
git stash push -m "auto-backup"
touch /tmp/backup-created
fi3. 不阻塞 —— 避免需要用户输入
Hook 在后台运行,没有交互界面。任何需要用户输入的操作都会导致 Hook 挂起直到超时。
❌ git commit(会打开编辑器等待 commit message)
❌ read -p "确认删除?(y/n)" answer(stdin 不可用)
❌ npm login(需要交互输入用户名密码)
✅ git stash push -m "auto-backup"(完全自动化)
✅ node automated-check.js(脚本内做决策,不需要外部输入)4. 错误容忍 —— Hook 失败不破坏工作流
除了 PreToolUse(失败会阻止操作),其他事件的 Hook 应该设计为"最好有,没有也行"。
#!/bin/bash
# ✅ 好的错误处理:失败时优雅降级
npx prettier --write "$CLAUDE_TOOL_OUTPUT_FILE" 2>/dev/null || true
# 如果 prettier 未安装或失败,静默跳过
# ❌ 差的错误处理:失败时污染 Claude 的输出
npx prettier --write "$CLAUDE_TOOL_OUTPUT_FILE"
# 如果 prettier 未安装,错误堆栈会打印到 Claude 能看到的地方,影响后续对话5. 团队共享 —— 提交到 Git vs 本地配置
| 配置级别 | 适合存放的内容 | Git 管理 |
|---|---|---|
settings.json(项目) | 团队共享的 Hooks:统一的 lint/format 流程 | ✅ 提交 |
settings.local.json(项目) | 个人偏好的 Hooks:桌面通知、日志路径 | ❌ 不提交 |
~/.claude/settings.json(全局) | 跨项目通用的个人 Hooks | ❌ 不提交 |
例子:
// .claude/settings.json — 团队共享,提交到 Git
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "npx prettier --write ${CLAUDE_TOOL_OUTPUT_FILE}"
}
]
}
}
// .claude/settings.local.json — 个人偏好,不提交
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "osascript -e 'display notification \"Code updated\"'"
}
]
}
}团队新人 clone 项目后,自动获得统一的格式化 Hook;同时每个人可以配置自己的通知、日志等个性 Hook。
6. 测试先行 —— 手动验证再启用
不要直接把 Hook 写到 settings.json 然后祈祷它生效。遵循三步测试流程:
第一步:独立运行脚本
# 模拟事件,手动运行 Hook 脚本
CLAUDE_TOOL_NAME="Write" \
CLAUDE_TOOL_OUTPUT_FILE="./src/test.ts" \
bash -c 'npx prettier --write ${CLAUDE_TOOL_OUTPUT_FILE}'第二步:观察真实触发
先在 settings.local.json 中配置 Hook(不影响团队),进行几次 Claude Code 操作后检查日志:
# 开启调试模式,观察 Hook 触发情况
DEBUG=claude-code:hooks claude第三步:推广到团队
验证通过后,将 Hook 从 settings.local.json 移到 settings.json,提交到 Git:
git add .claude/settings.json
git commit -m "feat: 添加 PostToolUse Hook 自动格式化代码"7. 日志记录 —— 留痕方便排查
Hook 在后台静默运行,出问题时很难排查。主动记录日志对调试至关重要。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "bash -c '\n LOG_FILE=\"/tmp/claude-code-hooks.log\"\n echo \"[$(date -Iseconds)] PostToolUse: ${CLAUDE_TOOL_NAME} -> ${CLAUDE_TOOL_OUTPUT_FILE}\" >> \"$LOG_FILE\"\n npx prettier --write \"${CLAUDE_TOOL_OUTPUT_FILE}\" 2>&1 | while read line; do\n echo \"[$(date -Iseconds)] prettier: $line\" >> \"$LOG_FILE\"\n done\n'"
}
]
}
}日志输出示例:
[2026-05-30T14:23:01+08:00] PostToolUse: Write -> /Users/me/project/src/utils.ts
[2026-05-30T14:23:01+08:00] prettier: src/utils.ts 62ms
[2026-05-30T14:23:15+08:00] PostToolUse: Edit -> /Users/me/project/src/index.ts
[2026-05-30T14:23:15+08:00] prettier: src/index.ts 48ms有了日志,当 Hook "似乎没生效"时,你可以立刻知道是没触发、触发了但执行失败、还是执行了但效果不是你预期的。
18.7 本章小结
本章深入讲解了 Claude Code 的 Hooks 钩子系统——事件驱动的自动化机制。以下是核心要点:
理解 Hooks:
- Hooks 是事件驱动的回调,类比 Git hooks 但在 Claude Code 的生命周期节点触发
- 6 种事件:PreToolUse、PostToolUse、SessionStart、Stop、PreCompact、Notification
- PreToolUse 能阻止操作(返回非 0),其他事件只做副作用
配置语法:
matcher:正则表达式过滤触发条件,越精确越好command:Shell 命令或脚本路径,支持${变量名}引用事件数据- 同一事件多个 Hooks 按数组顺序串行执行
5 大实用场景:
- PostToolUse 自动格式化(prettier / ESLint)
- PreToolUse 自动备份(git stash)
- PreToolUse 危险命令拦截(正则匹配
rm -rf等) - SessionStart 环境加载(Git 信息、Node 版本)
- PostToolUse 自动 lint 检查
7 条最佳实践:
- 快速执行(< 1 秒)
- 幂等性(重复运行安全)
- 不阻塞(不需要用户输入)
- 错误容忍(失败不影响工作流)
- 团队共享(settings.json 提交 Git,settings.local.json 存个人偏好)
- 测试先行(手动模拟 → 本地验证 → 团队推广)
- 日志记录(留痕方便排查)
Hooks 是 Claude Code 扩展生态的最后一块拼图。MCP 让你连接外部服务,Skills 让你封装工作流,Plugins 让你安装社区功能,而 Hooks 让你在正确的时间、自动做正确的事——不需要 Claude 主动调用,不需要你手动触发。
从下一章开始,我们将进入第五篇「实战篇」,用 8 个真实开发场景将前四篇的知识融会贯通。