教程Claude Code系列第 4 / 11 篇

Claude Code Hooks 实战:改完自动整理格式、拦住不该动的文件、等你批准时发通知

Hooks 是你写的 shell 命令,Claude Code 在固定时机一定会执行,不取决于模型愿不愿意。本站实测了三个:改完文件自动跑 eslint --fix,拦下对受保护文件的修改,等待批准时提醒你。附配置、脚本、真实的拦截画面,和它管不到的地方。

发布 · 约 10 分钟读完 · 经人工审核()· 本站的内容怎么来的

本站已实测,测试日期:2026-10-11。

本页目录(11 节)
  1. 先看懂一个 hook 长什么样
  2. 例一:改完文件,自动整理格式
  3. 例二:拦住不该动的文件
  4. 例三:它在等你的时候,提醒你
  5. 用 /hooks 查看
  6. 还有哪些事件
  7. 几条要知道的限制
  8. 不生效的时候怎么查
  9. 什么时候用 hook,什么时候不用
  10. 这一趟花了多少
  11. 这次实测没有覆盖到的

这篇教程解决一个问题:有些事你要 Claude Code 每一次都做,或者每一次都不许做,怎么保证。

写进 CLAUDE.md 是不够的。CLAUDE.md 那篇里说过,它的内容是交给模型参考的上下文,模型会尽量照做,但不保证。Hooks 不一样:它们是你写的 shell 命令,Claude Code 在固定的时机直接执行,不经过模型的判断。官方文档的说法是,hook 给你的是确定性的控制,某些动作一定会发生,而不是指望模型选择去做。

本站实测了最常用的三个,配置和脚本都可以直接拿去改。

实测环境
日期 2026-10-11
版本 Claude Code 2.1.296
系统 Ubuntu 24.04,已安装 jq
模型 Opus 5.5
连接方式 通过酷站AI 中转站,不是直连官方
示例仓库 Express 的源码,v5.3.0

先看懂一个 hook 长什么样

Hook 写在设置文件的 hooks 块里。三层结构:哪个事件、匹配什么、执行什么。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "这里写要执行的命令" }
        ]
      }
    ]
  }
}
部分 意思
PostToolUse 事件:在一次工具调用成功之后
matcher 过滤:只在工具是 Edit 或 Write 时触发。不写就每次都触发
command 要执行的 shell 命令

写在哪个文件,决定它管多大范围:

位置 范围 能不能共享
~/.claude/settings.json 你的所有项目 不能,只在本机
.claude/settings.json 这一个项目 能,可以提交进仓库
.claude/settings.local.json 这一个项目 不能,不进 git

下面三个例子,前两个写在项目的 .claude/settings.json 里,团队共用。

例一:改完文件,自动整理格式

要解决的事:它每次改完 JavaScript 文件,都按项目的 lint 规则自动修一遍格式,不用你提醒,也不用它记得。

用 PostToolUse 事件,匹配 Edit|Write:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | grep -E '\\.js$' | xargs -r npx eslint --fix"
          }
        ]
      }
    ]
  }
}

这条命令做了三件事。事件触发时,Claude Code 会把这次工具调用的信息以 JSON 的形式从标准输入传给命令;jq 从里面取出被改的文件路径;grep 只留下 .js 文件;最后交给 eslint --fix。官方文档的例子用的是 Prettier,我们换成了 Express 自己在用的 ESLint,另外加了 grep 那一段,免得改 Markdown 时也去跑 lint。

实测。 让它写一个故意格式不对的文件:

用 Write 工具新建 notes/fmt.js:一个函数 add(a, b),返回 a + b,并导出它。函数体故意用 4 个空格缩进,
return 那一行行尾多留 3 个空格。写完不要自己整理格式,也不要跑 lint 和测试。

写完后我们用 cat -A 看文件的实际内容:

function add(a, b) {$
  return a + b;$
}$
$
module.exports = add;$

缩进是 2 个空格,行尾没有多余的空格。hook 生效了。

有意思的是它自己的反应。它发现文件和它写的不一样,去查了原因,然后这样报告:

hook 改了文件。 .claude/settings.json 里配了一个 PostToolUse hook,每次 Edit/Write 一个 .js 文件后都会
自动跑 npx eslint --fix。它把 4 空格缩进改成了 2 空格,行尾空格就算有也会被删掉。这一步是 harness 跑的,
我拦不住。

“我拦不住”这四个字就是 hook 和 CLAUDE.md 的区别。

官方文档提醒:hook 成功时,对话里什么都不显示。要确认它跑了,就像我们这样去看文件。

例二:拦住不该动的文件

要解决的事:有些文件不许它改,比如 .env、锁文件、.git/ 下的东西,或者像 Express 的 History.md 这样只在发版时由维护者手工更新的文件。

这次用 PreToolUse,它在工具调用执行之前触发,可以把调用拦下来。逻辑稍多,写成一个脚本。保存为 .claude/hooks/protect-files.sh:

#!/bin/bash
# protect-files.sh:拦下对受保护文件的修改

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

PROTECTED_PATTERNS=(".env" "History.md" ".git/")

for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH 是受保护的文件(匹配 '$pattern')。History.md 只在发版时由维护者手工修改。" >&2
    exit 2
  fi
done

exit 0

脚本改自官方文档的例子。关键在最后几行的退出码:

退出码 意思
0 没有意见。注意这不等于批准,正常的权限流程照常走
2 拦下这个动作。写到标准错误的内容会交给 Claude,让它知道为什么
其他 当作 hook 自己出错了,动作照常进行

所以拦截的理由要写清楚,那是写给模型看的。

给脚本加上执行权限,再在设置里登记:

chmod +x .claude/hooks/protect-files.sh
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}

$CLAUDE_PROJECT_DIR 是项目的根目录,用它可以避免相对路径找不到脚本。

先手工测一遍脚本。 这是官方文档推荐的排错办法,不用启动 Claude Code 就能验证:

echo '{"tool_name":"Edit","tool_input":{"file_path":"/home/ccdemo/demo/a2-express/History.md"}}' | ./.claude/hooks/protect-files.sh
echo "退出码: $?"

我们得到的是那句 Blocked: 和 退出码: 2;把路径换成 lib/utils.js,退出码是 0。

实测。 让它去改 History.md:

现在在 History.md 最上面加一行:* demo: hook 测试

第一次的结果出乎意料,它根本没有去试:

这行我没有加。项目的 PreToolUse hook(.claude/hooks/protect-files.sh)把 History.md 列为受保护文件,
脚本里写的规则是:“History.md 只在发版时由维护者手工修改”。

技术上我可以用 Bash 写进去绕开这个 hook,因为它只拦 Edit/Write。但这等于跳过你们自己设的保护规则,
所以我不会自己这么做。

它先读了那个脚本,知道会被拦,就没有动手。为了看到拦截本身的样子,我们让它照做一次:

● Update(History.md)
  ⎿  Error: PreToolUse:Edit hook error: ["$CLAUDE_PROJECT_DIR"/.claude/hooks/protect-files.sh]: Blocked:
     /home/ccdemo/demo/a2-express/History.md 是受保护的文件(匹配 'History.md')。History.md 只在发版时由维护者手工修改。

● Bash(git status --short History.md; head -2 History.md)
  ⎿  5.3.0
     =====

● Edit 被 hook 拦下了,History.md 没有被改动。

修改没有发生,脚本写到标准错误的那句话原样交给了它。

这个 hook 管不到的地方

上面那段回答里,它自己把漏洞说出来了:这个 hook 只匹配 Edit 和 Write,用命令行改文件它看不见。 例一里它也提过同样的事:改用 Bash 的 heredoc 写文件,格式化的 hook 就不会触发。

这次它选择了不绕过,但这是模型的判断,不是保证。在权限模式那篇的实测里,它有两次就是用命令而不是编辑工具改的文件。

官方文档对此有明确的提醒,并给了几个办法:

  • 想按每次调用来管,就让 matcher 同时匹配 Bash,在脚本里用 git status --porcelain 列出被改动和新增的文件。
  • 想每一轮结束时统一检查一次,用 Stop 事件扫一遍工作区。
  • 想盯住某个具体的文件,不管是谁改的,用 FileChanged 事件。

另外,“不许读、不许改某个文件”这类要求,用权限规则里的 deny 往往更直接,写法见权限模式那篇。

例三:它在等你的时候,提醒你

要解决的事:你让它干活,自己切去做别的。它停下来等你批准的时候,你不知道。

用 Notification 事件。在有桌面的电脑上,官方文档给的命令是:

系统 命令
macOS osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"'
Linux notify-send 'Claude Code' 'Claude Code needs your attention'

这类个人习惯适合放在 ~/.claude/settings.json 里。macOS 的完整写法:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

matcher 留空表示所有类型的通知都触发。常用的两个取值:

matcher 什么时候触发
permission_prompt 它需要你批准一次工具调用,而且确认框已经等了大约 6 秒
idle_prompt 它答完大约 60 秒了,你还没有输入

实测。 我们的环境是一台没有桌面的服务器,notify-send 用不了(官方文档也说了,无界面的服务器和 SSH 会话没有通知服务)。所以把命令换成往文件里写一行,来验证事件确实触发了:

{
  "matcher": "permission_prompt",
  "hooks": [
    {
      "type": "command",
      "command": "echo \"$(date '+%H:%M:%S') Claude Code 在等你批准\" >> \"$CLAUDE_PROJECT_DIR\"/.claude/notify.log"
    }
  ]
}

切到 Manual 模式,让它跑一条需要批准的命令,然后不去管它:

时间 日志文件
确认框出现后 4 秒 还没有内容
确认框出现后 13 秒 17:01:44 Claude Code 在等你批准

和文档说的“大约 6 秒”对得上:你马上就处理的确认框不会打扰你,放着不管的才会提醒。

macOS 上如果通知不出现,官方文档给了原因:osascript 的通知是通过“脚本编辑器”这个应用发的,它没有通知权限时命令会悄悄失败。先在终端里运行一次那条命令,再到“系统设置 › 通知”里给脚本编辑器打开权限。这一条我们没有实测。

用 /hooks 查看

在会话里输入 /hooks,可以看到当前生效的所有 hook,按事件分组,并标明来自哪个设置文件:

  Hooks
  3 hooks on 3 events

  This menu is read-only. To add or change a hook, edit settings.json or ask Claude.

    PreToolUse  before tool execution
  ❯ [Project] "$CLAUDE_PROJECT_DIR"/.claude/hooks/protect-files…   Edit|Write · command

    PostToolUse  after tool execution
    [Project] jq -r '.tool_input.file_path' | grep -E '\.js$' |…   Edit|Write · command

    Notification  when notifications are sent
    [Project] echo "$(date '+%H:%M:%S') Claude Code 在等你批准"…   permission_prompt · command

这个菜单是只读的。要改 hook,直接改设置文件,或者让 Claude 帮你写。官方文档说,会话运行期间改了设置文件,通常会自动生效,不用重启。

还有哪些事件

官方文档列了三十多个事件。最常用的几个:

事件 什么时候触发 典型用法
PreToolUse 工具调用执行之前,可以拦 禁止某些命令、保护文件
PostToolUse 工具调用成功之后 格式化、跑 lint
UserPromptSubmit 你发出一句话、Claude 处理之前 给每句话附加背景信息
Stop Claude 答完的时候 检查没通过就不让它停
SessionStart 会话开始或恢复时 注入背景;配 compact 匹配可以在上下文压缩后把要紧的规矩重新塞回去
Notification Claude Code 发通知时 桌面提醒
PermissionRequest 它要向你请求批准时 自动批准你总是同意的那几种

除了执行命令,hook 还有另外几种类型:http 是把事件数据发到一个地址;prompt 是让一个模型做一次判断,适合“这件事算不算做完”这类不能用规则写死的问题;agent 是带工具的多轮核查,官方注明还是实验性的。

用 Stop hook 做“检查不过就接着干”的思路,和 /goal 是一回事,见Harness 工程和 Loop 工程是什么。

几条要知道的限制

按官方文档:

  • PostToolUse 撤销不了动作。 它触发时工具已经执行完了,只能事后补救。要拦就用 PreToolUse。
  • PreToolUse 的拒绝在任何权限模式下都有效,包括 bypassPermissions。这让你可以定一条换权限模式也绕不过去的规矩。反过来不成立:hook 返回“允许”,盖不过设置里的 deny 规则。
  • 同一个事件上的多个 hook 是并行跑的。 一个 hook 拒绝了,不会阻止其他 hook 执行。
  • 有超时。 命令类的 hook 默认 10 分钟,UserPromptSubmit 是 30 秒;可以用 timeout 字段单独设。
  • Stop 在它每次答完时都会触发,不只是任务完成时。
  • matcher 区分大小写。

要一次关掉所有 hook,在设置里写 "disableAllHooks": true。

不生效的时候怎么查

  1. 运行 /hooks,确认它出现在正确的事件下面。
  2. 检查 matcher 和工具名是否完全一致,包括大小写。
  3. 像上面那样,用 echo '...' | ./脚本 手工测一遍,看输出和退出码。
  4. 对话里出现 hook error 字样,说明脚本以意料之外的退出码结束了。提示找不到命令时,用绝对路径或者 $CLAUDE_PROJECT_DIR。
  5. 要看完整的错误输出,用 claude --debug 启动。

什么时候用 hook,什么时候不用

你想要的 用什么
它应该知道的背景和约定 CLAUDE.md
只在某些任务里用到的步骤 skill
每一次都必须发生的动作 hook
不许用某个工具、不许碰某个路径 权限规则里的 deny

判断的办法很简单:如果它偶尔不做也没关系,写进 CLAUDE.md;如果一次都不能漏,写成 hook。

这一趟花了多少

整个会话 /cost 显示 0.31 美元,模型工作 1 分 10 秒。这是 Claude Code 按官方价格估算的数字,不是中转站实际扣的金额。hook 本身是在你的机器上跑的 shell 命令,不消耗 token。

这次实测没有覆盖到的

  • 桌面通知没有实测。 服务器没有桌面,我们用写日志代替,验证的是事件会触发、大约在 6 秒后触发。
  • 只测了命令类的 hook,http、prompt、agent 三种没有测。
  • Stop、SessionStart、PermissionRequest 等事件没有测,按官方文档写的。
  • 例二里它没有绕过 hook,只看了这一次。 不要把它当成保证,该补的漏洞照上面的办法补。
  • Windows 上的写法没有涉及。 官方文档里有 PowerShell 的例子。

来源

通过 API 中转站使用这些模型?到 Veridrop 免费检测中转站给你的是不是它声称的模型,或者看看中转站排行榜前 100 名。

相关文章