Claude Code 的 Stop hook 会在主 Claude agent 每次回复结束时执行命令,所以很适合用来发送“已完成”通知、记录结果或运行检查。你中断 Claude 时它不会运行;如果某一轮因 API 错误结束,触发的则是 StopFailure。hook 会通过 last_assistant_message 拿到 Claude 的最后一条消息,也可以返回 decision: "block" 并附上 reason,让 Claude 继续工作而不是停下。
01Stop 何时触发,何时不触发
Stop 在每一轮结束时都会触发,而不只是在整个任务完成时。Claude Code 并没有“任务”的概念,它只知道 Claude 停止了回复、正在等你输入。一个简短的问题会触发一次 Stop,两小时的重构也一样只触发一次。
以下情况不会触发:
- 按下 Esc 或以其他方式中断本轮时;
- 本轮因 API 错误结束时,例如触发速率限制、服务过载或认证失败。这类情况下 Claude Code 会触发
StopFailure,并带有可用于匹配的error字段(rate_limit、overloaded、server_error等); - subagent 完成时。那对应的是
SubagentStop。
Stop 不支持 matcher,添加的 matcher 都会被忽略,hook 每一轮都会运行。
02最简单的 Stop hook
把下面的配置加到 ~/.claude/settings.json,就能在 Claude 每次完成时执行命令。这个例子会在 macOS 上播放提示音:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "afplay /System/Library/Sounds/Glass.aiff"
}
]
}
]
}
}
Mac、Windows 和 Linux 上的桌面通知命令,请参阅如何在 Claude Code 完成时收到通知。本文接下来讲的是如何利用 hook 的输入和输出。
03hook 收到的内容
除了通用字段(session_id、transcript_path、cwd、permission_mode、hook_event_name),Stop hook 还会收到:
last_assistant_messageClaude 本轮最后一条回复的文本stop_hook_active如果 Claude 已经因为 Stop hook 的拦截而在继续工作,则为 truebackground_tasks仍在运行的任务,例如后台 shell 或 subagentsession_crons本会话中已计划的唤醒,例如来自 /loop请使用 last_assistant_message,不要去读 transcript 文件。transcript 是异步写入的,hook 运行时可能还没写入最后一条消息。
通过 background_tasks 和 session_crons 可以判断会话是真的结束了,还是只是暂停、等后台任务再次唤醒。通知脚本可以先确认两者都为空,再提示“已完成”。
04把 Claude 的最后一条消息放进通知
这个脚本(适用于 macOS 和 Linux,需要 jq)会显示 Claude 回复的开头部分,如果后台任务还在运行则不发通知:
#!/bin/bash
# ~/.claude/hooks/notify-done.sh
input=$(cat)
# Skip if background tasks are still in flight
busy=$(jq '(.background_tasks // []) | length' <<<"$input")
[ "$busy" -gt 0 ] && exit 0
msg=$(jq -r '(.last_assistant_message // "Claude has finished")
| gsub("\\s+"; " ") | .[0:140]' <<<"$input")
if command -v osascript >/dev/null 2>&1; then
osascript -e 'on run argv' \
-e 'display notification (item 1 of argv) with title "Claude has finished"' \
-e 'end run' "$msg"
elif command -v notify-send >/dev/null 2>&1; then
notify-send "Claude has finished" "$msg"
fi
exit 0
保存脚本,运行 chmod +x ~/.claude/hooks/notify-done.sh,然后让 hook 指向它:
{
"hooks": {
"Stop": [
{
"hooks": [{ "type": "command", "command": "~/.claude/hooks/notify-done.sh" }]
}
]
}
}
这个脚本总是以 0 退出。这点很重要:在 Stop 中,退出码 2 表示“不要停止”,如果通知脚本意外以 2 退出,Claude 就会一直工作下去。
05让 Claude 继续工作
Stop 是可以拦截的事件之一。拦截停止意味着 Claude 会继续本轮工作,并把你给出的原因作为下一条指令。有两种做法:
- 以
2退出,Claude 会把你的 stderr 当作原因;或者 - 以
0退出并输出 JSON:
{
"decision": "block",
"reason": "The test suite is failing. Run npm test, fix the failures, then finish."
}
如果 hook 是在给出指导而不是报告错误,也可以用 hookSpecificOutput.additionalContext 让对话继续,这样 transcript 会将其标记为 Stop hook 反馈,而不是 hook 错误:
{
"hookSpecificOutput": {
"hookEventName": "Stop",
"additionalContext": "Please run the test suite before finishing"
}
}
避免无限循环
一个总是拦截的 Stop hook 会让 Claude 永远停不下来。有两重保护:
- 一旦 Claude 已经因为 Stop hook 而在继续工作,
stop_hook_active就会是true。检查它并提前退出。 - 如果 Stop hook 连续拦截八次且中间没有任何工具调用,Claude Code 会强制结束本轮。可以通过环境变量
CLAUDE_CODE_STOP_HOOK_BLOCK_CAP提高这个上限。
下面这个 hook 会让 Claude 修复一次失败的测试,然后允许它停止:
#!/bin/bash
# ~/.claude/hooks/tests-must-pass.sh
input=$(cat)
[ "$(jq -r '.stop_hook_active' <<<"$input")" = "true" ] && exit 0
if ! npm test --silent >/dev/null 2>&1; then
jq -nc '{decision: "block", reason: "npm test is failing. Fix the failures, then finish."}'
fi
exit 0
Hook 在会话的工作目录中运行,所以 npm test 会在你的项目里执行。检查要尽量快:默认情况下,command hook 最长可运行 600 秒,期间 Claude 会一直等待。
如果只是想让 Claude 朝某个目标持续工作、又不想写脚本,可以用 Claude Code 内置的 /goal 命令,它会自动为你设置一个仅限当前会话的 Stop hook。
06失败时也发通知
由于 API 错误不会触发 Stop,“已完成”提醒不会告诉你 Claude 因为速率限制而中止了。如果想收到这类通知,把同一个脚本也加到 StopFailure 下即可。它的输出和退出码都会被忽略,所以只能用来通知或记录日志。对于 StopFailure,last_assistant_message 中是错误信息,例如 "API Error: Rate limit reached"。
07Claude Code Notifier 的定位
用一个调用 push 服务的 Stop hook,就能在你离开时得知 Claude 已经完成,这个办法完全可行。如果想自己动手,可以从 ntfy 指南开始。
Claude Code Notifier 用的也是 Stop hook,区别只有一点:它能回传内容。它的免费命令行工具 ccn 支持 Mac、Windows 和 Linux,单独使用就能显示桌面通知。配对 iPhone 或 Android 应用后,每轮结束时你会立刻收到“Claude 已完成”的 push,还可以直接在手机上输入下一条指令。ccn 会通过 Stop hook 把它作为 Claude 的下一条指令返回,这样你不在键盘前会话也能继续。如果你在最近 30 秒内打过字,就不会等待手机输入。本工具为独立开发,并非 Anthropic 出品。相关工作流见离开长时间任务,安装请看 ccn 页面。
08常见问题
Stop hook 是在 Claude 完成整个任务时运行,还是每次回复后都运行?
每次回复后都会运行。无论请求大小,只要主 agent 结束响应,Stop 就会触发。如果需要知道是否还有后续工作在排队,可以检查输入中的 background_tasks 和 session_crons。
为什么我的 Stop hook 没有触发?
可能是你中断了这一轮,这样会跳过 Stop;也可能是这一轮因 API 错误结束,此时触发的是 StopFailure。否则,请确认 /hooks 中列出了你的 hook,并且脚本有可执行权限。其他情况见排查清单。
为什么我的 Stop hook 运行后 Claude 还在继续工作?
你的 hook 阻止了停止:要么以退出码 2 退出,要么输出了 decision: "block"。确保只做通知的 hook 始终以 0 退出,而所有会阻止停止的 hook 都先检查 stop_hook_active。
如何在 hook 中拿到 Claude 的最终回答?
从 stdin 的 JSON 中读取 last_assistant_message,例如用 jq -r '.last_assistant_message'。不要依赖 transcript_path:Stop 触发时,该文件里可能还没有最后一条消息。
Stop 和 SubagentStop 有什么区别?
Stop 针对主 agent。SubagentStop 在 subagent 完成时触发,按 agent 类型匹配,同样支持阻止相关的字段。“Claude 已完成”这类提醒应该放在 Stop 上。