在使用 Codex 时,每当一个完整的对话轮次结束或需要请求执行许可时,macOS 的通知中心都会即时推送通知给我。

起初我并未深究这一功能的实现方式,直到通过 tmux 使用 Codex 时,发现通知完全消失了,这才引起了我的好奇。 为什么会这样呢?

Codex 是如何实现原生系统通知

我们先从了解 Codex 原生系统通知的实现机制开始。

Codex 通过 OSC 9(Operating System Command sequences,操作系统命令序列)来触发系统通知。

这一功能最初似乎是 iTerm2 的特有实现,但我在 Ghostty 终端中也能正常使用。 如果你使用其他终端模拟器,可以用以下命令测试是否能够正常唤起系统通知:

printf '\033]9;%s\a' "This is the notification text"

其中 \033] 是 ANSI Escape Sequences 中的 OSC(操作系统命令序列)。 \a 对应 ASCII BEL(响铃,0x07),在 OSC 中常作为结束符(terminator)。

那为什么在 tmux 中失效了

这是因为 tmux 的 OSC 解析器只“认识/处理”少数几个 OSC 编号, 其它一律当成 “unknown” 丢弃(仅记录调试日志),不会转发给外层终端。

因此通知就无法送达了。不过,tmux 其实提供了一种解决方案。让我详细说明一下。

折中的方案

方案一:DCS 透传

tmux 提供了 DCS passthrough 功能,只需将需要透传的转义序列按以下格式包裹即可:

printf '\033Ptmux;\033%s\033\\' "$(printf '\033]9;%s\a' "This is the notification text")"

其中 \033P 是 ANSI 转义序列中的 DCS(Device Control String),它以 \033\\ 作为结束符。

方案二:直接写入客户端 TTY

通过获取当前 tmux 客户端的 TTY 地址,直接向其写入转义序列:

tty="$(tmux display-message -p '#{client_tty}' 2>/dev/null || true)"
printf '%b' "$(printf '\033]9;%s\a' "This is the notification text")" >"$tty"

如果需要定义通知标题,可以使用 OSC 777 序列:

printf '\033]777;notify;%s;%s\a' "This is the title" "This is the text"

现在就差如何让 Codex 调用这些方法了。

TL;DR

Codex 原生系统通知配置方式

增加一个 notify.sh 脚本,负责接收 Codex 的调用并解析其参数。

#!/usr/bin/env bash

set -eo pipefail

_write() {
	local msg="$1"
	if [ -n "$TMUX" ]; then
		# tmux inside:write into tty directly
		local tty
		tty="$(tmux display-message -p '#{client_tty}' 2>/dev/null || true)"
		if [[ -n "$tty" && -w "$tty" ]]; then
			printf '%b' "$msg" >"$tty"
		else
			# fallback: DCS passthrough + double ESC
			printf '\033Ptmux;\033%s\033\\' "$msg"
		fi
	else
		# tmux outside:direct OSC 9
		printf '%b' "$msg"
	fi
}

# OSC 9: notify with a single message string.
notify_osc9() {
	local msg="$1"
	_write $'\033]9;'"${msg}"$'\a'
}

# OSC 777 (Blink-style): notify;Title;Body
# Note: Title/Body must not contain unescaped ';' ideally.
notify_osc777() {
	local title="$1"
	local body="$2"

	# Basic sanitation: replace newlines with spaces; replace ';' to avoid field split.
	title="${title//$'\n'/ }"
	body="${body//$'\n'/ }"
	title="${title//;/:}"
	body="${body//;/:}"

	_write $'\033]777;notify;'"${title}"$';'"${body}"$'\a'
}

main() {
	[[ $# -ge 1 ]] || exit 0

	local json="$1"

	# Fast type check
	local type
	type="$(jq -r '.type // empty' <<<"$json" 2>/dev/null || true)"
	[[ "$type" == "agent-turn-complete" ]] || exit 0

	local last_msg input_messages thread_id title message

	last_msg="$(jq -r '."last-assistant-message" // "Turn Complete!"' <<<"$json")"
	input_messages="$(jq -r '."input-messages" // [] | join(" ")' <<<"$json")"
	thread_id="$(jq -r '."thread-id" // ""' <<<"$json")"

	title="Codex: ${last_msg}"
	message="${input_messages}"

	# Prefer OSC 777 (Title/Body)
	notify_osc777 "$title" "$message"
	# notify_osc9 "${title}\n${message}"
}

main "$@"

然后在 Codex 的配置文件(通常是 ~/.codex/config.yaml)中添加以下配置:

notifications:
  enabled: true
  command: /path/to/your/notify.sh

这样当 Codex 完成一个对话轮次时,就会调用你的脚本发送系统通知了。

Claude 原生系统通知配置方式

用以下代码替换以上 notify.sh 脚本中的 main 函数,新建 ~/.claude/scripts/notify.sh 即可。

main() {
	local json
	json="$(cat)"

	# Fast type check
	local type
	type="$(jq -r '.hook_event_name // empty' <<<"$json" 2>/dev/null || true)"
	case "$type" in
	"Notification")
		local notification_type title message

		notification_type="$(jq -r '."notification_type" // "Unknown"' <<<"$json")"

		title="Claude: ${notification_type}"
		message="$(jq -r '.message // empty' <<<"$json")"
		;;
	"Stop")
		title="Claude: ${type}"
		message="All done!"
		;;
	*)
		exit 0
		;;
	esac

	# Prefer OSC 777 (Title/Body)
	notify_osc777 "$title" "$message"
	# notify_osc9 "${title}\n${message}"
}

然后在 Claude 的配置文件(通常是 ~/.claude/settings.json )中添加以下配置片段:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/scripts/notify.sh"
          }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/scripts/notify.sh"
          }
        ]
      }
    ]
  }
}

本文基于个人实践经验编写,并使用 Avante + DeepSeek V3.2 进行了润色优化。