When using Codex, whenever a complete conversation turn ends or when execution permission is requested, macOS's Notification Center immediately pushes a notification to me.

At first, I didn't delve deeply into how this feature was implemented, until I used Codex through tmux and discovered that notifications had completely disappeared. This sparked my curiosity. Why is this happening?

How Codex Implements Native System Notifications

Let's start by understanding Codex's native system notification implementation mechanism.

Codex triggers system notifications through OSC 9 (Operating System Command sequences).

This feature initially appears to be a iTerm2-specific implementation, but I can also use it normally in Ghostty terminal. If you use other terminal emulators, you can test whether system notifications can be properly triggered with the following command:

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

Where \033] is the OSC (Operating System Command sequence) in ANSI Escape Sequences. \a corresponds to ASCII BEL (bell, 0x07), which is often used as a terminator in OSC.

Why Does It Fail in tmux

This is because tmux's OSC parser only "recognizes/processes" a few OSC numbers, and treats all others as "unknown" and discards them (only logging debug messages), without forwarding them to the outer terminal.

Therefore, notifications cannot be delivered. However, tmux actually provides a solution. Let me explain in detail.

Compromise Solutions

Solution 1: DCS Passthrough

tmux provides a DCS passthrough feature. You just need to wrap the escape sequences to be passed through in the following format:

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

Where \033P is the DCS (Device Control String) in ANSI escape sequences, which uses \033\\ as a terminator.

Solution 2: Write Directly to Client TTY

By obtaining the current tmux client's TTY address, write escape sequences directly to it:

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

If you need to define a notification title, you can use the OSC 777 sequence:

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

Now we just need to figure out how to make Codex call these methods.

TL;DR

Codex Native System Notification Configuration

Add a notify.sh script that is responsible for receiving calls from Codex and parsing its arguments.

#!/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 "$@"

Then add the following configuration to Codex's configuration file (usually ~/.codex/config.yaml):

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

This way, when Codex completes a conversation turn, it will call your script to send system notifications.

Claude Native System Notification Configuration

Replace the main function in the notify.sh script above with the following code, and create ~/.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}"
}

Then add the following configuration snippet to Claude's configuration file (usually ~/.claude/settings.json):

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

This article is based on personal practical experience and was polished and optimized using Avante + DeepSeek V3.2.