Hooks:在生命周期节点强制执行动作

提示词是「希望它这么做」,钩子是「必定这么做」。这是本手册最技术的一节,但有三个用法值得任何人配一次。

约 10 分钟

这是全手册最技术的一节。 但先说结论:

有三个钩子值得任何人配一次,每个不到十行。 配完就不用再管它了。

要不要往下读完整节,看你需不需要第四个。

钩子是什么

在特定时刻自动执行的一条命令。

它和写在 CLAUDE.md 里的要求的区别:

CLAUDE.md / 技能 钩子
性质 希望它这么做 必定这么做
谁执行 模型(它得先理解,再记得) 程序
会不会漏 不会
占上下文 不占

一句话:需要判断的用提示词,需要保证的用钩子。

值得任何人配的三个

① 它需要你的时候提醒你

最实用的一个。 你派了个长任务,切去做别的,它做完或者需要你批准时,你不知道。

macOS:

JSON
{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code 需要你\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

放进 ~/.claude/settings.json

更简单的办法:如果只是想要个响声,不用配钩子——在设置里把 preferredNotifChannel 设成 "terminal_bell" 就行。

② 挡住不该碰的文件

比在 CLAUDE.md 里写「不要动这些文件」硬得多。

先写一个脚本 .claude/hooks/protect.sh

Terminal
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

PROTECTED=(".env" "client-data/" "台账")

for p in "${PROTECTED[@]}"; do
  if [[ "$FILE_PATH" == *"$p"* ]]; then
    echo "已阻止:$FILE_PATH 命中受保护规则 '$p'" >&2
    exit 2
  fi
done

exit 0

记得给执行权限chmod +x .claude/hooks/protect.sh

然后在设置里挂上:

JSON
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect.sh"
          }
        ]
      }
    ]
  }
}

这个钩子有一个很硬的性质,值得单独说:

PreToolUse 钩子在所有权限检查之前触发,在每一种权限模式下都触发——包括 bypassPermissions返回拒绝的钩子,在跳过权限的模式下也照样能挡住。

反过来不成立:钩子说「允许」,挡不住设置里的拒绝规则。钩子只能收紧,不能放松。

③ 压缩之后把关键信息塞回去

做长任务时很有用。 压缩会丢掉一些东西,这个钩子在压缩后自动把要点补回去:

JSON
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo '提醒:原话摘录必须逐字。受访者一律用编号。输出到 coding.md。'"
          }
        ]
      }
    ]
  }
}

原理是SessionStart 这类事件下,钩子打印到标准输出的任何内容都会进入它的上下文。

配置放哪

位置 范围
~/.claude/settings.json 你的所有项目
.claude/settings.json 这个项目(可以进版本库)
.claude/settings.local.json 这个项目,只有你

/hooks 查看当前配了哪些——它是只读的,要改还是得编辑文件,或者直接让它帮你改。

改完通常自动生效,不用重启。

结构

JSON
{
  "hooks": {
    "事件名": [
      {
        "matcher": "筛选条件",
        "hooks": [
          { "type": "command", "command": "要执行的命令" }
        ]
      }
    ]
  }
}

⚠️ 一个常见的错误:已经有 hooks 这个键的时候,新事件要作为兄弟键加进去,不要把整个对象替换掉。

常用的事件

事件有三十多个,日常用得上的是这几个:

事件 什么时候 能不能拦
PreToolUse 动手之前
PostToolUse 动完之后 不能(已经做了)
Notification 需要你的时候 不能
SessionStart 会话开始或压缩之后 不能
UserPromptSubmit 你按下回车之后、它开始处理之前
Stop 它说完一轮之后
SessionEnd 会话结束 不能

退出码决定一切

这是钩子机制里唯一必须记住的规则:

退出码 含义
0 正常。SessionStart 这类事件下,标准输出的内容会进入它的上下文
2 拦住。写到标准错误的内容会作为反馈发给它
其他 出错了但不拦,动作照常继续

⚠️ 不要混用两种方式。 要么用「退出码 2 + 标准错误」来拦,要么用「退出码 0 + 结构化 JSON」来做精细控制——退出码是 2 的时候,JSON 会被忽略。

还有一条容易误解的

退出码 0 不等于「批准」。PreToolUse 来说,0 只是「我不表态」,正常的权限流程照走。

matcher 怎么写

写法 匹配
省略、"""*" 全部
"Bash" 只有 Bash
"Edit|Write" 两者之一
"mcp__.*" 所有 MCP 工具(这是正则

规则是:只含字母数字、下划线、连字符、空格、逗号、竖线的,当作精确匹配或列表;含其他字符的,当作正则

区分大小写。bash 匹配不上 Bash

不同事件的 matcher 筛的东西不一样:工具类事件筛工具名,SessionStart 筛启动方式(startup / resume / clear / compact / fork),Notification 筛通知类型。

两个不用写脚本的类型

如果你不想碰 shell,还有两种:

让一个小模型来判断

JSON
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "检查这一轮是不是把所有要求的核对步骤都做了。没做完就回 {\"ok\": false, \"reason\": \"还差什么\"}。"
          }
        ]
      }
    ]
  }
}

默认用 Haiku,很快也很便宜。

让一个带工具的代理来验证

JSON
{
  "type": "agent",
  "prompt": "验证输出文件里的条数和原始材料对得上。$ARGUMENTS"
}

这个能真的去跑命令查。 标注为实验性,可能变化。

怎么选:光看输入数据就能判断 → 用 prompt需要去核对实际情况 → 用 agent

几个会踩到的坑

Stop 钩子的连续拦截有上限

Stop 钩子每次它说完话都会触发,不只是任务完成时。

如果你的 Stop 钩子一直拦着不让停,连续 8 次之后 Claude Code 会强行覆盖它。

正确的写法是检查那个标志位:

Terminal
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
# 后面才是你的逻辑

钩子拦不住通过命令做的事

PreToolUse 挡住「编辑文件」这个工具,但挡不住它跑一个脚本、脚本自己去改文件。

要每一次文件变更都被看到,加一个 Stop 钩子每轮扫一遍工作树。

shell 配置里的 echo 会破坏 JSON

一个很隐蔽的坑: 钩子会启动一个 shell,如果你的 ~/.zshrc 里有无条件的 echo,那行输出会跑到钩子的 JSON 前面,导致解析失败。

改法是把它包进交互式判断:

Terminal
if [[ $- == *i* ]]; then
  echo "Shell ready"
fi

多个钩子的结果怎么合并

匹配同一事件的钩子会全部跑完,然后才合并结果。

一个钩子返回「拒绝」,不会阻止它的兄弟钩子执行。 别指望用一个钩子的拒绝去抑制另一个钩子的副作用。

权限决定按最严格的算:拒绝 > 延后 > 询问 > 允许。

排查

钩子不触发:

  1. /hooks 确认它挂在正确的事件下
  2. 检查 matcher 的大小写
  3. 手工测一下:
Terminal
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $?

报「command not found」: 用绝对路径,或者用 ${CLAUDE_PROJECT_DIR} 开头,并确认给过执行权限。

看执行细节:Ctrl + O 打开完整记录。成功的钩子什么都不显示;被拦的显示标准错误;出错的显示一行错误提示。

最后一条建议

别一上来就配一堆。

先配那个通知钩子。 它立刻就有用,而且不会出错。

然后等你真的遇到「它又忘了做某件事」的时候,再配第二个。

钩子是解决具体问题的工具,不是需要提前搭建的框架。

本节事实查证日期:2026-08-05。 依据:官方 Hooks guide 与 Hooks 参考页。

← 回到手册目录