How to get notified when Codex finishes
Codex has a single notify hook in config.toml that runs a command when a turn ends. Here is the minimal setup, the JSON it passes as an argument, and the ways it is not like Claude Code hooks.
Codex does not have Claude Code's hook system. It has one key, notify, in ~/.codex/config.toml, and it runs a command when a turn completes. That is the whole surface, and it is enough for "tell me when it is done".
The smallest version that works on macOS:
notify = ["osascript", "-e", 'display notification "Codex finished" with title "Codex"']
Restart Codex, run a prompt, and you get a notification when the turn ends.
One TOML detail will silently eat your config: notify must be a top-level key. config.toml is full of [section] headers, and if you paste notify = [...] below one of them, it becomes [tui].notify or [projects].notify and does nothing. Put it on the first line of the file and you cannot get this wrong.
Why it is an array
Claude Code hooks take a shell command as a string. Codex notify takes an argv array and executes it directly, with no shell. That means:
- No pipes, no
&&, no output redirection. - No environment variable expansion.
$HOMEstays a literal$HOME. - No
~. Use absolute paths.
If you want any shell behavior at all, point notify at a script and put the shell logic in the script:
notify = ["/Users/you/bin/codex-notify.sh"]
Remember chmod +x on the script. A non-executable script fails silently here, because Codex discards the error.
The payload arrives as an argument, not stdin
This is the part that trips up everyone who set up Claude Code hooks first. Claude Code writes its JSON to your hook's stdin. Codex appends the JSON as one extra argument at the end of your argv. In a script that is $1:
{
"type": "agent-turn-complete",
"thread-id": "b5f6c1c2-1111-2222-3333-444455556666",
"turn-id": "12345",
"cwd": "/Users/you/project",
"input-messages": ["Rename foo to bar and update the callsites."],
"last-assistant-message": "Rename complete and verified cargo build succeeds."
}
Two fields are worth using. cwd tells you which project finished, which matters the moment you run more than one agent. last-assistant-message is a ready-made notification body.
Here is a script that uses both. It hands the JSON to Python and lets json.dumps produce the AppleScript string literals, so a quote in the message cannot break out of the osascript expression:
#!/bin/sh
payload="$1"
[ -n "$payload" ] || exit 0
printf '%s' "$payload" | /usr/bin/python3 -c '
import json, os, subprocess, sys
d = json.load(sys.stdin)
project = os.path.basename(d.get("cwd", "")) or "Codex"
msg = (d.get("last-assistant-message") or "Turn finished")[:120]
subprocess.run(["osascript", "-e",
"display notification " + json.dumps(msg)
+ " with title " + json.dumps(project + " finished")])
' &
exit 0
Prefer a sound? Swap the osascript call for ["afplay", "/System/Library/Sounds/Glass.aiff"].
What Codex will not tell you
The differences from Claude Code hooks are easy to summarize:
| Claude Code hooks | Codex notify | |
|---|---|---|
| Config | ~/.claude/settings.json, per event |
one top-level key in config.toml |
| Payload | JSON on stdin | JSON as the last argv argument |
| Events | start, stop, permission prompt, more | turn complete, and only that |
| Execution | shell command, Claude waits for it | direct exec, fire and forget |
Two of those rows have real consequences.
There is no start event. You can know when a turn ended but never when it began, so anything that wants to measure the wait, or show state while Codex works, cannot be built on notify alone. We hit this building Unwait: the Claude Code integration shows cards during the wait, the Codex integration can only announce that the wait is over.
notify does not fire on approval prompts. A turn that is blocked waiting for you to approve a command looks exactly like a turn that is still working. If you walk away trusting the notification, a blocked agent will sit there for as long as you do.
Fire and forget also changes debugging. Codex spawns your command and moves on, with stdout and stderr discarded, so an echo in your script goes nowhere. If the hook seems dead, log to a file:
printf '%s\n' "$1" >> /tmp/codex-notify.log
Run a prompt, then check the file. If JSON shows up, Codex is calling you and the bug is in your script. If nothing shows up, the config line is wrong, and the first thing to check is whether notify sits under a [section] header.
The upside of fire and forget: a slow notify script cannot hang your session. Claude Code hooks run in the turn's critical path and a hung hook hangs the session. Codex does not wait for you, so the backgrounding discipline that Claude Code hooks force on you is optional here. Keep it anyway if the same script serves both.
The zero-setup alternative
Recent Codex builds can post notifications through the terminal itself:
[tui]
notifications = true
You can also filter to specific types:
[tui]
notifications = ["agent-turn-complete", "approval-requested"]
Note approval-requested in that list. The TUI path covers the blocked-agent case that notify cannot see, which makes it the better pick if a notification is all you want.
The catch is that it works through terminal escape codes, so it depends on your terminal. iTerm2, WezTerm, and Ghostty handle it; Apple's Terminal does not. And it can only notify. If you want the event to reach anything outside a notification bubble, a daemon, a log, another machine, you are back to notify.
One key means one owner
Because notify is a single key rather than a list of hooks, tools that integrate with Codex compete for it. Unwait sets it, and other tools do the same. Before you overwrite the line, look at what is there: we have seen a tool take over notify and chain the previous script behind its own, which works until either tool tries to clean up after itself.
If you hand-edit and something else later claims the key, your notification quietly stops. When a hook that worked for weeks goes silent, check notify first.
A last note on longevity: in the Codex source, notify is now implemented as a compatibility layer named legacy_notify on top of a newer lifecycle-hooks system. It still works and is still the documented interface as of this writing, but if you are reading this long after the publish date, check whether the hooks system has grown a public config surface, because it fires on more events than notify ever will.
Further reading
Codex configuration is documented in the openai/codex repository. For the Claude Code side of the same problem, see How to get notified when Claude Code finishes.