Claude Code 完整指南(四):Hooks(自动化事件触发)
文章目录
- 1. 引言
- 2. Hooks 是什么?
- 3. Hooks 配置(用户级 vs 项目级)
- 4. 八种事件类型汇总
- 5. 命令型 Hooks
- 5.1 基础语法(最常用)
- 5.2 Hook 能拿到什么上下文?
- 5.3 退出码怎么控制“阻断/不阻断”?
- 6. 提示型 Hooks
- 6.1 基础语法
- 5.2 响应格式(核心)
- 7. 案例模板
- 7.1 案例:SessionStart 做环境体检(不阻断)
- 7.2 案例:PreToolUse 拦截危险 Bash(阻断)
- 7.3 案例:PreToolUse 做敏感信息检测(阻断)
- 7.4 案例:PostToolUse 自动格式化 + 自动跑测试(不阻断)
- 7.5 案例:Notification 做系统通知(不阻断)
- 8. 常见问题
- 9. 文末
1. 引言

在前面的博客,博主已经讲解了 Claude Code 相关的概念,有兴趣的同学可以参考下:
- 《Claude Code 完整指南(一):安装、CLI 实战、IDE 集成一次讲透》
- 《Claude Code 完整指南(二):终端命令全解析(收藏级)》
- 《Claude Code 完整指南(三):命令背后的数据流动》
如果你已经在用 Claude Code 写代码,你一定遇到过这些“重复但必要”的动作:
- 写完文件要跑格式化、lint、单测
- 运行危险 Bash 之前想要一层保险(尤其是
rm -rf、git push --force) - 想在会话开始时自动检查环境(Node/Python 版本、依赖是否安装)
- 想在 Claude “准备停下”时强制做一次自检:测试过了吗?有未提交的改动吗?
Hooks 的价值就在于:把这些动作从“ 你记得做” 变成 “系统自动做” ,并且可以做到“该阻断就阻断、该后台跑就后台跑、该提醒就提醒”。
2. Hooks 是什么?
Hooks 是 Claude Code 的事件驱动自动化系统:在特定事件触发时,自动执行一组 Hook。
它主要有两种形态:
- 命令型 Hooks(command):运行 Shell 命令/脚本,适合自动化与校验(格式化、检查、通知、跑测试……)。
- 提示型 Hooks(prompt):在
Stop/SubagentStop事件里,让 Claude 再做一次“停下前的质量检查”,根据结果决定继续还是停止。
你可以把它理解为:
- SessionStart/End:会话生命周期钩子
- PreToolUse/PostToolUse:工具执行前/后钩子(Write/Edit/Bash/Read…)
- Stop/SubagentStop:模型准备结束前的“收尾门禁”
- Notification:系统通知的自定义处理
3. Hooks 配置(用户级 vs 项目级)
Claude Code 支持分层配置,Hooks 通常按是否团队共享来决定目录。
项目级(推荐团队共享):
- 文件:
{project}/.claude/settings.json - 适合:团队统一的格式化、测试、危险命令拦截、质量门禁等
- 配套建议:把脚本也放进仓库,例如
{project}/.claude/hooks/*.sh,让团队开箱即用
项目级本地(仅自己用,不提交):
- 文件:
{project}/.claude/settings.local.json - 适合:个人路径相关脚本、个人通知方式、私有工具等
- 配套建议:加入
.gitignore
用户级(跨项目生效):
- 文件:
~/.claude/settings.json - 适合:你个人的通用 Hooks(比如任何项目都启用的通知/危险命令拦截)
小贴士:团队规范建议 “项目级为主、用户级为辅”,项目级让仓库可复制;用户级放个人偏好。
4. 八种事件类型汇总
| 事件 | 触发时机 | Hook 类型 | 常见用途 |
|---|---|---|---|
| SessionStart | 会话开始 | 命令型 | 初始化环境、检查依赖、打印提示 |
| SessionEnd | 会话结束 | 命令型 | 清理临时文件、记录统计 |
| UserPromptSubmit | 用户提交输入前 | 命令型 | 输入验证、关键词拦截、注入上下文 |
| PreToolUse | 工具执行前 | 命令型 | 危险命令拦截、参数校验、审计 |
| PostToolUse | 工具执行后 | 命令型 | 自动格式化、自动测试、生成变更摘要 |
| Stop | Claude 决定停止前 | 提示型 | 质量门禁(lint/test/git status) |
| SubagentStop | 子代理停止前 | 提示型 | 子任务验收(比如“测试是否补齐”) |
| Notification | Claude 发送通知时 | 命令型 | 转发通知(系统通知/Slack/飞书等) |
5. 命令型 Hooks
写脚本就能自动化
5.1 基础语法(最常用)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "./.claude/hooks/format.sh",
"blocking": false
}
]
}
]
}
}
关键点:
matcher:用于筛选工具(通常写正则/或模式串),比如Write|Edit、Bash。blocking:true:Hook 失败会阻断当前动作(适合安全校验、门禁)false:Hook 失败不阻断(适合格式化、统计、通知)
5.2 Hook 能拿到什么上下文?
命令型 Hooks 通常可以通过环境变量拿到上下文(不同事件变量略有差异,常见如):
| 变量名 | 说明 |
|---|---|
| TOOL_NAME | 工具名称(如 Write/Edit/Bash) |
| TOOL_INPUT | 工具输入(JSON 字符串) |
| TOOL_INPUT_FILE_PATH | 文件路径(Write/Edit 常用) |
| TOOL_OUTPUT | 工具输出(PostToolUse 常用) |
| USER_MESSAGE | 用户输入(UserPromptSubmit) |
| CLAUDE_WORKING_DIR | 当前工作目录 |
| NOTIFICATION_MESSAGE | 通知内容(Notification,JSON) |
更稳妥的写法:脚本里同时支持读取
stdin(JSON)与环境变量,避免某些环境差异导致取不到值。
5.3 退出码怎么控制“阻断/不阻断”?
常见约定(建议你按这个写,团队最好统一):
exit 0 # 成功,继续
exit 2 # 阻断错误(blocking=true 时会拦下本次操作)
exit 1 # 非阻断错误(记录/提示,但不中断主流程)
6. 提示型 Hooks
让 Claude 在“停下前”做质量门禁
提示型 Hooks 仅用于 Stop / SubagentStop:当 Claude 觉得“差不多可以结束了”,会先跑一次这个提示,让它自检并输出 JSON 决策。
6.1 基础语法
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "检查是否满足停止条件。返回 JSON: {"decision":"stop"|"continue","feedback":"说明"}"
}
]
}
]
}
}
5.2 响应格式(核心)
{
"decision": "continue",
"feedback": "请先运行测试并修复 lint。"
}
你可以用它来实现 “Claude 自己给自己验收” 的体验,不满足条件就继续把事情做完,满足才停。
7. 案例模板
下面的模板尽量遵循一个原则:高风险校验 blocking=true,耗时工作 blocking=false。
依赖提示:后面的脚本示例会用到
jq来解析 JSON(macOS 可用brew install jq)。
7.1 案例:SessionStart 做环境体检(不阻断)
session-init.sh:检查 Node/Python/Git 状态、依赖是否安装等(按你的项目改)。
.claude/settings.json配置如下:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "./.claude/hooks/session-init.sh", "blocking": false }
]
}
]
}
}
session-init.sh 示例代码如下:
#!/usr/bin/env bash
set -euo pipefail
echo "🚀 Claude Code SessionStart"
echo "cwd: ${CLAUDE_WORKING_DIR:-$(pwd)}"
if command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1; then
echo "git branch: $(git branch --show-current)"
changes="$(git status --porcelain | wc -l | tr -d ' ')"
if [[ "${changes}" != "0" ]]; then
echo "⚠️ uncommitted changes: ${changes}"
fi
fi
if [[ -f package.json ]]; then
command -v node >/dev/null 2>&1 && echo "node: $(node --version)"
[[ -d node_modules ]] || echo "📦 tip: run npm install"
fi
if [[ -f requirements.txt ]]; then
command -v python >/dev/null 2>&1 && echo "python: $(python --version 2>&1 | head -n 1)"
[[ -n "${VIRTUAL_ENV:-}" ]] || echo "🐍 tip: activate venv"
fi
7.2 案例:PreToolUse 拦截危险 Bash(阻断)
guard-bash.sh思路:从TOOL_INPUT(或 stdin JSON)里取出command,匹配rm -rf、git push --force等高危模式,命中就exit 2并在 stderr 输出原因。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "./.claude/hooks/guard-bash.sh", "blocking": true }
]
}
]
}
}
guard-bash.sh 示例代码如下:
#!/usr/bin/env bash
set -euo pipefail
input_json="${TOOL_INPUT:-}"
if [[ -z "${input_json}" ]]; then
input_json="$(cat || true)"
fi
cmd="$(echo "${input_json}" | jq -r '.command // empty' 2>/dev/null || true)"
if [[ -z "${cmd}" ]]; then
exit 0
fi
if [[ "${cmd}" == *"[claude-allow-danger]"* ]]; then
exit 0
fi
deny_patterns=(
'(^|[[:space:]])rm[[:space:]]+-rf[[:space:]]+(/|~|$HOME)([[:space:]]|$)'
'(^|[[:space:]])git[[:space:]]+push([[:space:]].*)?--force'
'(^|[[:space:]])git[[:space:]]+reset([[:space:]].*)?--hard'
'(^|[[:space:]])chmod[[:space:]]+-R[[:space:]]+777'
)
for pattern in "${deny_patterns[@]}"; do
if echo "${cmd}" | grep -Eq "${pattern}"; then
echo "❌ 已阻断高危命令(PreToolUse):" >&2
echo " ${cmd}" >&2
echo "如确认要执行,请在命令末尾加 [claude-allow-danger] 作为显式确认。" >&2
exit 2
fi
done
exit 0
7.3 案例:PreToolUse 做敏感信息检测(阻断)
适用场景:Write/Edit 写入内容前,扫描是否疑似 Key/密码/Token,命中直接拦截。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "./.claude/hooks/check-secrets.sh", "blocking": true }
]
}
]
}
}
check-secrets.sh示例代码如下:
#!/usr/bin/env bash
set -euo pipefail
input_json="${TOOL_INPUT:-}"
if [[ -z "${input_json}" ]]; then
input_json="$(cat || true)"
fi
content="$(echo "${input_json}" | jq -r '.new_string // .content // ""' 2>/dev/null || true)"
patterns=(
'AKIA[0-9A-Z]{16}' # AWS Access Key
'ghp_[a-zA-Z0-9]{36}' # GitHub PAT
'sk-[a-zA-Z0-9]{32,}' # API Key(示例)
'(?i)passwords*[:=]s*["'][^"']+["']'
'(?i)api[_-]?keys*[:=]s*["'][^"']+["']'
)
for pattern in "${patterns[@]}"; do
if echo "${content}" | grep -Pq "${pattern}" 2>/dev/null; then
echo "❌ 检测到潜在敏感信息,已阻断写入(PreToolUse)" >&2
echo "pattern: ${pattern}" >&2
echo "建议:改用环境变量/密钥管理服务,不要把 Key 写进仓库。" >&2
exit 2
fi
done
exit 0
7.4 案例:PostToolUse 自动格式化 + 自动跑测试(不阻断)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "./.claude/hooks/format.sh", "blocking": false },
{ "type": "command", "command": "./.claude/hooks/auto-test.sh", "blocking": false }
]
}
]
}
}
format.sh 示例代码如下:
#!/usr/bin/env bash
set -euo pipefail
file="${TOOL_INPUT_FILE_PATH:-}"
[[ -n "${file}" && -f "${file}" ]] || exit 0
case "${file}" in
*.ts|*.tsx|*.js|*.jsx|*.json)
command -v prettier >/dev/null 2>&1 && prettier --write "${file}" >/dev/null 2>&1 || true
command -v eslint >/dev/null 2>&1 && eslint --fix "${file}" >/dev/null 2>&1 || true
;;
*.py)
command -v black >/dev/null 2>&1 && black "${file}" >/dev/null 2>&1 || true
command -v isort >/dev/null 2>&1 && isort "${file}" >/dev/null 2>&1 || true
;;
*.go)
command -v gofmt >/dev/null 2>&1 && gofmt -w "${file}" >/dev/null 2>&1 || true
;;
*.rs)
command -v rustfmt >/dev/null 2>&1 && rustfmt "${file}" >/dev/null 2>&1 || true
;;
esac
exit 0
auto-test.sh 示例代码如下:
#!/usr/bin/env bash
set -euo pipefail
file="${TOOL_INPUT_FILE_PATH:-}"
[[ -n "${file}" && -f "${file}" ]] || exit 0
if echo "${file}" | grep -Eq '(.test.|.spec.|/tests?/|__tests__)'; then
exit 0
fi
if [[ -f package.json ]] && command -v npm >/dev/null 2>&1; then
if echo "${file}" | grep -Eq '.(ts|tsx|js|jsx)$'; then
echo "🧪 running minimal js/ts tests (best effort)"
npm test --silent 2>/dev/null || true
fi
fi
if [[ -f pyproject.toml || -f requirements.txt ]] && command -v pytest >/dev/null 2>&1; then
if echo "${file}" | grep -Eq '.py$'; then
echo "🧪 running minimal python tests (best effort)"
pytest -q 2>/dev/null || true
fi
fi
exit 0
建议:
- 格式化脚本按后缀选择
prettier/black/gofmt/rustfmt - 测试脚本“只跑与当前文件相关的最小集合”,避免每次都全量测试拖慢体验
7.5 案例:Notification 做系统通知(不阻断)
notify.sh(macOS)可以用osascript或terminal-notifier,从NOTIFICATION_MESSAGE里取title/message发系统通知。
{
"hooks": {
"Notification": [
{
"hooks": [
{ "type": "command", "command": "./.claude/hooks/notify.sh", "blocking": false }
]
}
]
}
}
notify.sh 示例脚本如下:
#!/usr/bin/env bash
set -euo pipefail
msg_json="${NOTIFICATION_MESSAGE:-}"
if [[ -z "${msg_json}" ]]; then
msg_json="$(cat || true)"
fi
title="$(echo "${msg_json}" | jq -r '.title // "Claude Code"' 2>/dev/null || echo "Claude Code")"
message="$(echo "${msg_json}" | jq -r '.message // empty' 2>/dev/null || true)"
[[ -n "${message}" ]] || exit 0
if command -v terminal-notifier >/dev/null 2>&1; then
terminal-notifier -title "${title}" -message "${message}" -group "claude-code" >/dev/null 2>&1 || true
exit 0
fi
osascript -e "display notification "${message}" with title "${title}"" >/dev/null 2>&1 || true
exit 0
8. 常见问题
问题一:Hook 不执行
可以通过如下方式排查:
开调试:
claude --debug="hooks"
检查脚本可执行权限:
chmod +x ./.claude/hooks/*.sh
脚本自测(用 stdin 模拟 JSON):
echo '{"sessionId":"test"}' | ./.claude/hooks/session-init.sh
问题二:无故被阻断
blocking=true 的 Hook 一定要做到:
- 只在命中明确条件时才
exit 2 - stderr 输出清晰原因(让 Claude 能把“为什么被拦”解释给你)
问题 三:变慢很明显
- 把耗时任务(全量测试、扫描整个仓库)改成
blocking=false或改成“增量/抽样” - 在脚本里加超时/短路条件(比如只对特定目录、特定后缀执行)
9. 文末
通过阅读本文,相信大家已经系统理解了 Claude Code 中 Hooks 的整体设计与实战价值:它并不是零散的脚本技巧,而是一套事件驱动的自动化与质量门禁机制。通过在 Session、Tool 执行前后以及 Stop 阶段合理配置命令型与提示型 Hooks,可以将格式化、测试、危险命令拦截、敏感信息校验等“重复但关键”的操作交由系统自动完成,把人为自觉升级为流程约束。
希望本文能对大家深入理解和落地使用 Claude Code Hooks 有所帮助,也欢迎在评论区分享你的实战配置、踩坑经验与优化思路。感谢阅读,本文完!











