文章比较事件钩子、终端响铃与屏幕推断三种状态感知方式,其中钩子能提供更明确的事件信息。示例为 Claude Code 和 Codex 共用桌面通知脚本,并提醒处理钩子标准输出的协议约束。
书籍:LLM Observability Pocket Guide
我的其他作品:Thinking in Go(两本系列)—— Go 编程完整指南 + Go 中的六边形架构
我的项目:Hermes IDE | GitHub —— 为使用 Claude Code 和其他 AI 编程工具交付软件的开发者打造的 IDE
关于我:xgabriel.com | GitHub
两个终端,两个 Agent,都已经安静了四分钟。一个正在阅读四十个文件,还没开始写第一行代码;另一个从第一分钟起就停在权限确认 prompt 上,等着你按下那个迟迟没按的键。从另一个窗口看过去,或者站在房间另一头看,它们完全一样。
Agent 之外的任何东西,都无法确定地区分这两种情况。Agent 必须主动告诉你自己处于哪种状态。目前,编程 Agent 有几种方式可以做到这一点,但它们传达的信息量不同。hook 可以准确告诉你发生了什么。终端响铃只会说“看我一下”,除此之外什么也不说。如果 Agent 什么信号都不发,那就只剩下读屏幕、靠猜了。
Claude Code 和 Codex 都能在特定事件发生时执行命令,所以先准备一个两者共用的小脚本。它会在 macOS 或 Linux 上显示桌面通知,而且不输出任何内容。有些 Codex hook 会把 stdout 当作决策结果读取,或者拒绝纯文本输出,因此通知脚本应该保持静默。
#!/bin/sh
# agent-ping: desktop notification on macOS or Linux
# usage: agent-ping "Title" "Message"
exec >/dev/null 2>&1
title=${1:-Agent}
msg=${2:-Needs your attention}
case "$(uname -s)" in
Darwin)
osascript -e 'on run argv' \
-e 'set t to item 1 of argv' \
-e 'set m to item 2 of argv' \
-e 'display notification m with title t' \
-e 'end run' "$title" "$msg"
;;
Linux)
notify-send "$title" "$msg"
;;
esac
将它保存为 ~/bin/agent-ping,执行 chmod +x ~/bin/agent-ping,然后先用 ~/bin/agent-ping Test Hello 测试,再接入其他配置。Anthropic 的 hooks 指南提醒了两个坑:在 macOS 上,osascript 通知通过 Script Editor 发送,如果 Script Editor 没有通知权限,命令就会“静默失败”;在 Linux 上,notify-send 需要通知守护进程,而无桌面环境的服务器、SSH 会话和大多数容器都没有。同一份指南还提供了 Windows 的 PowerShell 版本。
从外部看,一个被阻塞的 Agent 和一个忙碌的 Agent 没什么区别。只有 Agent 自己知道处于哪种状态。

Claude Code 有 33 种 hook 事件。其中两种可以回答“我该不该看一下这个终端?”
Notification 在“Claude Code 发送通知时”触发,它的 matcher 按通知类型进行筛选。根据 hooks 指南,你需要关注两种类型:permission_prompt 在 Claude 需要你批准工具调用,并且“prompt 已经等待约六秒”时触发;idle_prompt 在“Claude 大约 60 秒前已经完成回复,而你之后一直没有输入”时触发。Notification hook 只负责观察。参考文档明确说,它们不能阻止或修改通知。
Stop 在“Claude 完成回复时”触发,没有延迟,也没有 matcher,因此每一轮都会执行。它的输入包含 last_assistant_message,也就是 Claude 最后一条回复的文本。将下面这两项加入 ~/.claude/settings.json:
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt|elicitation_dialog",
"hooks": [
{
"type": "command",
"command": "\"$HOME/bin/agent-ping\" Claude 'Needs you'"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$HOME/bin/agent-ping\" Claude 'Turn done'"
}
]
}
]
}
}
matcher 是用 | 分隔的通知类型列表,每个类型都按名称精确匹配。因此,permission_prompt|elicitation_dialog 还能捕获 MCP server 向你打开表单的情况,同样需要等待六秒。
带有 permission_prompt 的 Notification 意味着“正在等你处理”,只是晚了约六秒。Stop 会立即触发,但它只能说明这一轮结束了。Claude 究竟是完成了任务,还是在回复末尾向你提了一个问题,答案藏在 last_assistant_message 里;要区分这两种情况,就得阅读这段文本。这已经是在猜了。
这些 hook 会与 Claude Code 内置的提醒同时运行。默认情况下,Claude Code 只会在 Ghostty、Kitty 和 iTerm2 中发送桌面通知。终端指南介绍了如何通过 "preferredNotifChannel": "terminal_bell",让其他终端改用普通响铃。
Codex 将这件事拆到了三处设置中。最早的是 notify:每当 Codex 发出受支持的事件时,就会执行一个命令,并传入“单个 JSON 参数”。文档说明,目前支持的事件“只有 agent-turn-complete”。
所以,notify 能告诉你一轮执行结束了。对于审批,Codex 现在提供了“默认启用”的 hooks,其中 PermissionRequest“在 Codex 即将请求批准时运行”。PermissionRequest hook 也可以允许或拒绝请求,而通知脚本恰恰不该这么做。文档对此有说明:“如果没有匹配的 hook 作出决定,Codex 就使用正常的审批流程”,stdout 中的纯文本则会被忽略。agent-ping 不输出任何内容,因此 prompt 仍然由你来回答。
在 ~/.codex/config.toml 中加入:
notify = [
"/bin/sh", "-c",
"\"$HOME/bin/agent-ping\" Codex 'Turn done'",
]
Codex 会把它的 JSON 作为额外的一个参数追加到命令后面。使用 sh -c 时,这个参数会落到 $0,而上面的命令会忽略它。在 ~/.codex/hooks.json 中加入:
{
"hooks": {
"PermissionRequest": [
{
"hooks": [
{
"type": "command",
"command": "\"$HOME/bin/agent-ping\" Codex 'Needs you'"
}
]
}
]
}
}
Codex 不会立刻运行它:“在非托管 hook 运行之前,Codex 要求你审查并信任该 hook 的确切定义。”在 Codex 内运行 /hooks,信任一次即可。
如果你不想写任何脚本,TUI 也有自己的提醒功能。配置参考文档中,tui.notifications 可以设为 true,也可以设为事件类型列表,例如 agent-turn-complete 和 approval-requested。默认只有终端未获得焦点时才会触发,除非你设置了 notification_condition = "always":
[tui]
notifications = ["approval-requested"]
notification_method = "auto"
使用 auto 时,Codex“优先使用 OSC 9 通知”,否则“回退到 BEL(\x07)”。这些字节,任何终端程序都能发送。
在 hooks 出现之前,程序如果想引起你的注意,就会向自己的输出写入特殊字节,交给终端处理。下面四种如今仍然重要:
printf '\a'
printf '\033]9;%s\033\\' 'Codex needs you'
printf '\033]777;notify;%s;%s\033\\' 'Codex' 'Needs you'
printf '\033]99;;%s\033\\' 'Codex needs you'
第一种是 BEL,只有一个 0x07 字节。它完全不携带消息,而且很多其他情况也会触发它,比如 shell 在 Tab 补全失败时发出提示音。第二种是 OSC 9,iTerm2 将它定义为“发送通知”,内容是一个消息字符串。第三种是 OSC 777,即 rxvt-unicode 的 notify 扩展,带有标题和正文;WezTerm 同时支持 9 和 777,Ghostty 默认会将它们显示为桌面通知。第四种是 OSC 99,这是 kitty 自己的协议,kitty 也兼容旧的 OSC 9。
根据 Claude Code 的终端指南,如果你在 tmux 内运行 Claude Code,那么在添加 set -g allow-passthrough on 之前,它的桌面通知都无法传递到外层终端。在 tmux 内手动用 printf 输出的序列,也需要套上 tmux 自己的透传封装。
任何承载终端的应用都能看到这些字节流经,无须配置。问题是,它们所表达的只是“这个标签页里的某个东西需要你注意”。一次响铃无法区分“完成了”和“被阻塞了”。OSC 9 消息可以区分,但前提是有人阅读消息文本。
一种解决方案正在推进。2026 年 10 月 6 日,Mitchell Hashimoto 提出了 OSC 7501,也就是 Program Status Protocol。他给出的理由是:“我希望大家都别再通过读屏幕或进程树,来猜程序正在做什么。”一条状态报告会直接写明状态:
ESC ] 7501 ; state=blocked:kind=permission ESC \
该规范目前处于草案修订版 0.4,日期为 10 月 10 日,定义了 idle、working、done、blocked 和 error 这些状态;处于 blocked 状态时,kind 可以设为 permission、question 或 auth。Claude Code 的终端文档已经说明,从 v2.1.295 起,实现了该协议的终端可以显示每个会话“正在工作、正在等你,还是已经完成”,无须开启任何选项。规范仍是草案,因此在依赖它之前,先确认你的终端是否支持。
有些 CLI Agent 不发送上述任何信号,或者只响一声铃,除此之外什么也没有。想显示它们状态的工具,就只剩下一种输入:Agent 打印出来的文本流。Mitchell 的文章直白地点出了这种做法:“读取屏幕或窗口标题,再与已知模式匹配,以此进行猜测。”
读屏猜测大致就像下面这个玩具示例:
// a guess, not a signal
function guess(lastLines, quietMs) {
if (/\[y\/n\]|\(y\/n\)/i.test(lastLines)) return "asking?";
if (quietMs > 30_000) return "idle?";
return "working?";
}
每个分支都可能出错。模型处理一个很长的请求时,可能会安静三十秒。Agent 打印出来的文件里,可能恰好包含 (y/n)。厂商可能在下一个版本里改掉 prompt 的措辞。这些返回值里的问号,也应该出现在 UI 中。
读屏是最后的办法:它适用于任何 Agent,也可能在任何 Agent 身上判断出错。

把错误的信号解读为“被阻塞”,同样会带来麻烦。根据 hooks 指南,Claude Code 的 idle_prompt 会在 Claude“大约 60 秒前完成回复,而你之后一直没有输入”时触发。一分钟前就完成工作的 Agent,状态应该是完成。把这条通知当成“正在等你处理”,每一轮完成都会变成一次误报。
Hermes IDE 是我开发的,阅读这一节时,请把这一点考虑在内。Hermes 会在真正的终端里,并排运行各个 Agent 自己的界面,因此它会同时遇到上面所有信号,也会遇到什么信号都不发的 Agent。
如果手动接入上面的所有机制,同一次 Codex 审批请求可能会通知你两遍:一次来自 PermissionRequest hook,另一次来自 TUI 的 OSC 9 横幅。Hermes 会把收到的信息合并为每个会话的一个状态。README 列出的状态包括“工作中、需要批准、向你提问、已完成、出错或空闲”,而且状态“会说明何时是在猜测”。
同一份 README 还写道,Hermes“观察 Agent 主动报告的信息,以及 git 显示的信息”。它“绝不会替你回答权限确认 prompt,也绝不会自行向终端输入内容”,这与前面 agent-ping 坚守的是同一条界限。仓库中的 ADR 003 解释了它为什么以终端为核心,并保持厂商中立。
当 Agent 正在等你处理,或者已经完成时,系统通知会触发;对于你当前正在查看的会话,则不会通知。按下 ⌘I(Windows 和 Linux 上是 Ctrl+Shift+I),可以跳转到等待时间最长的 Agent,然后在那个 Agent 自己的终端里作答。
当状态明确写着它是在猜测时,请亲自查看终端。Hermes 可以免费下载,按 BSL 1.1 许可开放源码,支持 macOS、Windows 和 Linux,截至 10 月 13 日的版本是 2.3.3。它与 Nous Research、Anthropic、OpenAI 或任何 Agent 厂商都没有隶属关系。
Agent 状态是一个小规模的可观测性问题。你可以信任 Agent 主动发送的信号,而任何从输出中推断出来的信息,都应该视为猜测。LLM Observability Pocket Guide 将同样的原则应用于 LLM 应用的 tracing 和 evals。如果你想同时关注多个 Agent,又不想手动配置 hooks,可以下载 Hermes IDE;如果它帮你及时处理了一次卡住的 prompt,也欢迎在 GitHub 上给它点个 star。

如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。