在 Codex 或 pi 需要你时收到手机通知
在服务器上运行的编程 Agent,等你的时间往往比工作的时间还长。Codex 停在审批提示上,pi 完成任务后等待下一条消息,而你只有在下次查看终端时才会发现。本文使用 Codex hooks、一个小型 pi 扩展和 ntfy,为这两个 Agent 设置推送到手机的通知。
下面的脚本已在 Linux 上使用 Codex CLI 0.167.1 和 pi 1.5.2 测试过,在 macOS 上同样适用。
为什么内置通知无法送达手机
Codex 有两种通知功能,但都无法覆盖这种情况:
- TUI 通知(
config.toml中的[tui] notifications)会发送桌面通知或触发终端响铃。它们只发送到连接着 Codex 的终端,因此分离的 tmux 会话不会产生任何你能看到的提示。 notify会运行你指定的程序,但 OpenAI 的高级配置指南指出,它目前只在agent-turn-complete时触发。它不会告诉你有审批在等待,而这恰恰是你最关心的事件。
Codex hooks 可以覆盖这两种事件。PermissionRequest 在 Codex 即将请求审批时运行,Stop 在一个 turn 结束时运行。pi 没有审批机制。它的扩展 API 提供了 agent_settled 事件,会在 pi 完成所有自动工作并开始等待输入时触发。
设置 ntfy
ntfy 会把 HTTP POST 请求作为推送通知送达。安装 Android 或 iOS 应用并订阅一个 topic。
公共的 ntfy.sh 服务器无需注册,因此 topic 名称就相当于密码:任何知道它的人都可以读取和发送消息。请生成一个较长的随机名称:
echo "agents-$(openssl rand -hex 12)" 在服务器上测试:
curl -d "Test from $(hostname)" https://ntfy.sh/YOUR_TOPIC 在 Android 上,请为该 topic 开启 instant delivery。ntfy 文档提醒,如果不开启,手机处于休眠模式时消息可能会明显延迟。
Codex:在审批请求和 turn 结束时通知
将以下脚本保存为 ~/.codex/hooks/ntfy.sh,并用 chmod +x 设为可执行。它需要 jq 和 curl。
#!/bin/sh
# Codex hook: push a phone notification through ntfy.
url=https://ntfy.sh/YOUR_TOPIC
event=$(cat)
field() { printf '%s' "$event" | jq -r "$1"; }
project=$(field '.cwd | split("/") | last')
case $(field .hook_event_name) in
PermissionRequest)
title="Codex needs approval: $project"
body=$(field '.tool_input.description // .tool_input.command // .tool_name')
priority=high
;;
Stop)
title="Codex finished: $project"
body=$(field '(.last_assistant_message // "Turn complete")[0:300]')
priority=default
;;
*) exit 0 ;;
esac
curl -fsS --max-time 5 -o /dev/null
-H "Title: $title" -H "Priority: $priority" -d "$body" "$url" || true 在 ~/.codex/config.toml 中为这两个事件注册它:
[[hooks.Stop]]
[[hooks.Stop.hooks]]
type = "command"
command = "~/.codex/hooks/ntfy.sh"
timeout = 10
[[hooks.PermissionRequest]]
[[hooks.PermissionRequest.hooks]]
type = "command"
command = "~/.codex/hooks/ntfy.sh"
timeout = 10 启动 codex 并运行 /hooks。Codex 会跳过新增或修改过的 hook,直到你审查并信任它为止,并且信任是按 hook 的哈希值记录的。之后修改脚本或配置条目,都需要重新信任,否则通知会停止,而你的手机上不会出现任何错误。
对于审批请求,Codex 通过 stdin 传入的 JSON 如下所示:
{
"hook_event_name": "PermissionRequest",
"cwd": "/home/agent/projects/api",
"permission_mode": "default",
"tool_name": "Bash",
"tool_input": {
"command": "touch /tmp/created.txt",
"description": "Allow creating /tmp/created.txt? The sandbox is read-only."
}
} Stop 事件携带的是 last_assistant_message,而不是 tool_input。两者都包含 session_id 和 turn_id。
需要注意的细节
- hook 必须保持静默。
PermissionRequesthook 可以通过打印决定来批准或拒绝请求。该脚本不打印任何内容并以 0 退出,因此 Codex 会显示正常的审批提示。curl -o /dev/null让 ntfy 的响应不进入 stdout,|| true防止网络故障被当作 hook 错误显示。 - 设置较短的超时。 hooks 同步运行,默认超时为 600 秒。如果 ntfy 无法访问,较长的超时会拖住审批提示本身。
- 你只会收到需要你处理的请求。 对于沙箱已经允许的命令,
PermissionRequest不会运行。使用workspace-write沙箱和on-request审批时,这意味着只有 workspace 之外的写入和网络访问会触发通知,而这些正是值得查看的请求。 Stop在每个 turn 都会触发, 包括交互式会话中的简短回复。如果觉得太吵,可以只注册PermissionRequest。- hooks 属于 Codex,而不是 TUI。 它们在
codex exec中同样会运行。
pi:在 pi 等待输入时通知
将以下内容保存为 ~/.pi/agent/extensions/ntfy.ts。pi 会自动从该目录加载扩展,/reload 可以在不重启的情况下载入修改。
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { basename } from "node:path";
const url = "https://ntfy.sh/YOUR_TOPIC";
export default function (pi: ExtensionAPI) {
pi.on("agent_settled", async (event, ctx) => {
if (event.aborted) return; // you pressed Escape, so you are already there
await fetch(url, {
method: "POST",
headers: { Title: `pi: ${basename(ctx.cwd)}` },
body: "Waiting for input",
}).catch(() => {});
});
} 请使用 agent_settled,而不是 agent_end。pi 的扩展文档说明,agent_end 会在每次底层运行后触发,之后 pi 可能会重试、压缩后重试,或继续处理排队的消息。基于 agent_end 的通知可能在 pi 仍在工作时就到达。
由于 pi 不会暂停等待审批,通知只能告诉你某次运行已经结束。如需在命令运行前阻止它,请参阅 pi 的安全指南。
不要让机密进入通知
审批描述、命令以及最后一条助手消息中,可能包含文件路径、主机名,或出现在命令行里的 token。在 ntfy.sh 上,它们会经过第三方服务器,并送达任何知道 topic 名称的人。
如果这对你的工作很重要,可以选择以下方式之一:
当通知不够用时
通知告诉你 Agent 需要你,但你仍然需要一种回应的方式。在手机上通过 SSH,你需要连接 tmux、向上滚动找到提示,然后在全屏 TUI 中输入。从手机访问编程 Agent 服务器介绍了这种设置的网络部分。
我们开发的工具 Pontia 会在手机浏览器中显示 Codex 审批请求,并提供 Allow 和 Deny 按钮,还会将 pi 会话以聊天形式实时显示。