跳转到内容

Hooks 深入与实战

CLAUDE.md 是「贴在墙上的公约」——大家都说好,但没人盯着你执行。Hooks(钩子)则像一位铁面门卫:站在工具调用的必经之路上,规则写进他的值班手册,符合就放行,违反就拦下。公约靠自觉,门卫靠强制。

这一篇把 9 个生命周期事件、matcher 语法、阻断机制和五组实战示例一次讲透。

Hooks 写在 settings.json(项目级 .claude/settings.json、用户级 ~/.claude/settings.json)的 hooks 字段下。也可以用 /hooks 命令交互式管理——加、删、看,全在命令面板里完成。

基本结构长这样:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write $CLAUDE_FILE_PATHS"
}
]
}
]
}
}

每个事件下挂一组规则,每条规则有 matcher(匹配哪些工具)和 hooks(执行什么命令)。

Hooks 挂在 9 个时间点上,覆盖一次会话从开始到结束的全程:

事件 触发时机 能否阻断
PreToolUse 工具调用之前 ✅ 可阻断该次调用
PostToolUse 工具调用之后 ❌(已执行完)
UserPromptSubmit 用户提交 prompt 之前 ✅ 可拦截输入
Notification Claude 发送通知时
Stop Claude 完成响应时 ✅ 可阻止结束
SubagentStop 子代理完成时 ✅ 可阻止子代理结束
PreCompact 执行 compact 之前
SessionStart 会话开始或恢复时
SessionEnd 会话结束时

最常用的是 PreToolUse(拦截危险操作)和 PostToolUse(事后清理,如格式化、跑测试)。

matcher 决定这条规则匹配哪些工具调用:

"matcher": "*" // 匹配所有工具
"matcher": "Bash" // 只匹配 Bash
"matcher": "Edit|Write" // 匹配 Edit 或 Write(管道 = 或)

想拦下所有写文件的动作?Edit|Write 即可。想盯住所有 bash 命令?Bash 一个就够。

stdin 与 exit code:门卫的对话方式

Section titled “stdin 与 exit code:门卫的对话方式”

Hook 命令通过 stdin 收到一段 JSON,里面带着这次调用的上下文。最关键的字段是 tool_input——比如 Bash 命令的 tool_input.command 就是即将执行的命令文本,Edit 的 tool_input 带着文件路径和改动内容。

{
"session_id": "abc123",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/important"
}
}
  • exit 0:放行,门卫点头。
  • exit 2阻断,并把 stderr 的内容反馈给 Claude。门卫不仅拦下,还递上一张条子说明为什么拦——Claude 读到后会调整下一步。
  • 其他非零 exit code:非阻断错误,记到日志里,但不拦工具调用。

exit 2 是最有用的一种:它让规则变成「可解释的强制」——不是闷头拦下让 Claude 一头雾水,而是告诉它「这条命令动了 .env,请换个做法」。Claude 读到 stderr 的反馈后,通常会换一条路径:比如你想删 .env,它被拦下后会改成「用 git restore 撤销改动」或「先备份再删」。这种「拦下 + 解释」的反馈环,比单纯拒绝更接近一位讲道理的门卫。

一个被忽视的组合:SessionStart 与 SessionEnd

Section titled “一个被忽视的组合:SessionStart 与 SessionEnd”

会话两端的钩子常被忽略,但用好了很省心。SessionStart 在新会话或 claude -c 恢复时触发,可以用来打印一段项目背景、加载当前 git 分支名;SessionEnd 在会话结束时触发,适合做收尾归档——比如把会话摘要写进项目日志,或统计本次 token 用量。它们不阻断流程,只在边界处悄悄记一笔,等于给每次会话装了「进出场打卡机」。

以下示例均来自官方 hooks-guide,可复制即用。

把 Claude 跑过的每条命令都存一份审计日志。

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command' >> ~/.claude/bash-audit.log"
}
]
}
]
}
}

从 stdin 读 JSON,用 jq 抽出命令文本,追加到日志文件。事后复盘「它到底跑了什么」一目了然。

2. 代码格式化 hook:编辑后自动跑 prettier

Section titled “2. 代码格式化 hook:编辑后自动跑 prettier”

每次 Edit/Write 之后,自动格式化被改的文件。

{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "prettier --write $(jq -r '.tool_input.file_path' || true)"
}
]
}
]
}
}

这样无论 Claude 写出的代码缩进多乱,落盘前都会被 prettier 拉直。

3. Markdown 格式化 hook:自动补语言标签

Section titled “3. Markdown 格式化 hook:自动补语言标签”

团队要求代码块必须带语言标签(```bash 而非裸 ```)。用 Python 脚本在写 Markdown 后自动补全:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/scripts/format-markdown.py"
}
]
}
]
}
}

format-markdown.py 读 stdin 拿到文件路径,扫描裸代码块,补上语言标签。规则交给机器执行,比在 CLAUDE.md 里反复叮嘱靠谱得多。

4. 自定义通知 hook:用 notify-send 提醒

Section titled “4. 自定义通知 hook:用 notify-send 提醒”

长任务跑完,桌面弹个通知。

{
"hooks": {
"Notification": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' '需要你注意一下'"
}
]
}
]
}
}

macOS 可换成 osascript -e 'display notification \"需要你注意\" with title \"Claude Code\"'

5. 文件保护 hook:阻止编辑敏感文件

Section titled “5. 文件保护 hook:阻止编辑敏感文件”

防止 Claude 误改 .envpackage-lock.json.git/ 下的文件。

{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | grep -Eq '(^|/)(\\.env|package-lock.json)(/|$)|/\\.git/' && { echo '禁止编辑敏感文件,请改用其他方式'; exit 2; } || exit 0"
}
]
}
]
}
}

exit 2 把「禁止编辑敏感文件」反馈给 Claude,它就知道换个思路而不是反复撞墙。这是门卫最经典的工作方式:拦下、解释、引导。

Hooks 自动运行,而且和你当前用户一样有权限——能读环境变量、能跑命令、能联网。这带来三个风险:

  1. 凭证泄露:Hook 能读到当前 shell 的所有环境变量,包括 API key、token。
  2. 恶意 hook:从别处拷来的 settings.json 里可能藏着一个 PreToolUse 钩子,悄悄把你代码外传。
  3. 供应链:你装的工具(如某个 prettier 插件)被投毒,hook 一跑就触雷。

防护建议:

  • 不从不信任的来源直接导入 settings.json,导入前人工审一遍 hooks 字段。
  • 限制 hook 命令的权限:能不联网就别联网,能只读就别写。
  • 定期审计:用 /hooks 看看当前挂着哪些钩子,确认都是你想要的。

Hook 没按预期工作?三步排查:

  1. /hooks:确认规则真的挂上了、matcher 写对没。

  2. 手动喂 JSON:把一段示例 stdin 喂给命令,看输出和 exit code。

    Terminal window
    echo '{"tool_input":{"file_path":".env"}}' | <你的 hook 命令>; echo $?
  3. 看 stderrexit 2 的反馈内容来自 stderr,把它打到日志确认 Claude 收到了什么。如果反馈含糊,Claude 可能反复撞同一堵墙;写清「为什么拦」比单纯「拦下」重要得多。

  4. 临时加日志:在命令前塞一行 echo "$@" >> /tmp/hook-debug.log,把每次收到的原始 JSON 存下来,回放排查最直观。

维度 CLAUDE.md Hooks
性质 建议性(advisory) 强制性(enforced)
执行 模型「尽量遵守」 100% 执行
适合 风格偏好、命名约定、上下文背景 安全红线、格式化、审计
失效方式 模型忘了就破例 几乎不会破例

经验之谈:把「应该这样」写进 CLAUDE.md,把「绝不能那样」交给 Hooks。前者靠模型自觉,后者靠门卫把门。两者配合,才是既灵活又可靠的纪律体系。


Hooks 让规则从「墙上的公约」变成「门口的门卫」。下一篇看 MCP 深入,了解怎么用万能插座接通外部服务。🚀