Earpiece

← Blog

Get a notification or voice alert when Codex CLI finishes

By Aditya Vernekar · · 4 min read

Codex CLI has two ways to tell you a turn is done. Its own terminal notifications are on by default and fire when the terminal isn’t focused. For anything custom, set notify in ~/.codex/config.toml: Codex runs that command after every turn and passes a JSON description of the turn as the last argument.

Codex is good at long, quiet stretches of work: a migration, a refactor across a dozen files. That’s exactly when you stop watching it. Here is what Codex gives you out of the box, how notify works, and how to go from a plain “done” to a spoken summary. Field names below come from the Codex source on GitHub, so you can rely on them in scripts.

Built-in: terminal notifications

The Codex TUI sends a notification through your terminal when a turn finishes. Three settings under [tui] in ~/.codex/config.toml control it:

~/.codex/config.toml
[tui]
notifications = true              # default
notification_method = "auto"      # "auto", "osc9" or "bel"
notification_condition = "unfocused"  # or "always"
  • osc9 asks the terminal for a desktop notification, in terminals that support it. bel rings the bell, which works almost everywhere.
  • unfocused only notifies when you’re looking at another window. always notifies regardless.

That’s enough for one session. It won’t tell you which project finished or what happened, and it can’t run your own command. For that, there is notify.

How notify works

notify is a top-level key holding the command as a list of arguments, without the payload. After each completed turn, Codex runs the command with one extra argument: a JSON string describing the turn.

~/.codex/config.toml
notify = ["notify-send", "Codex"]

runs, after every turn, something like:

shell
notify-send Codex '{"type":"agent-turn-complete","turn-id":"12345", ...}'

Two details that matter: it has to be a top-level key, so put it above any [section] in the file, or TOML will file it under that section. And Codex doesn’t wait for the command or show its output, so a broken script fails silently.

The payload

The only event type today is agent-turn-complete. The full payload looks like this:

json
{
  "type": "agent-turn-complete",
  "thread-id": "b5f6c1c2-1111-2222-3333-444455556666",
  "turn-id": "12345",
  "cwd": "/Users/you/projects/shop",
  "client": "codex-tui",
  "input-messages": ["Rename `foo` to `bar` and update the callsites."],
  "last-assistant-message": "Rename complete and verified `cargo build` succeeds."
}
  • thread-id identifies the conversation, so you can tell parallel sessions apart. turn-id identifies the turn.
  • cwd gives you the project.
  • input-messages is what you asked; last-assistant-message is Codex’s final reply.

Note the keys use hyphens, not underscores. In jq that means bracket syntax: .["last-assistant-message"]. Codex’s source code (legacy_notify.rs) describes notify as the legacy integration, kept for backward compatibility. It still works and is still the simplest way to run a command per turn.

A notification with the project and the reply

Save this as ~/.codex/notify.sh and make it executable with chmod +x ~/.codex/notify.sh:

bash
#!/bin/sh
# Codex passes the turn as JSON in the last argument.
payload="$1"
project=$(printf '%s' "$payload" | jq -r '.cwd | split("/") | last')
reply=$(printf '%s' "$payload" | jq -r '.["last-assistant-message"] // "Done" | .[0:180]')
osascript - "$project" "$reply" <<'EOF'
on run argv
  display notification (item 2 of argv) with title ("Codex · " & item 1 of argv)
end run
EOF

Then point Codex at it, using the full path:

~/.codex/config.toml
notify = ["/Users/you/.codex/notify.sh"]

Passing the text to osascript as arguments, not pasting it into the script, means quotes or backslashes in Codex’s reply can’t break the AppleScript. To hear it instead, replace the osascript block with printf '%s. %s' "$project" "$reply" | say.

The limits you’ll hit

  • One slot. notify holds a single command. If another tool already uses it, yours replaces it. Chain them in one script instead.
  • Every turn, even short ones. The payload has no start time or duration, so a script can’t tell a five-second reply from a twenty-minute refactor without keeping its own state.
  • Turn end only. notify doesn’t fire when Codex is waiting on an approval. For that, Codex’s newer lifecycle hooks (in ~/.codex/hooks.json, including PermissionRequest and Stop) are the way in. Codex ignores hooks you haven’t trusted, so run /hooks once to approve them.
  • Noise from the Codex app. The desktop app runs a hidden turn to title each thread, and notify reports it like any other turn.

Or let Earpiece handle it

Earpiece plugs into the same notify slot and turns each turn into one spoken sentence, like “billing. Migrated the invoices table and updated the tests.” Specifically:

  • It keeps your existing notify command. If you already have one, earpiece install --chain runs Earpiece first and then yours, with a guard so a wrapper that calls Earpiece back can’t loop. earpiece uninstall puts your command back.
  • Each turn is spoken once, even if Codex delivers it twice, and the thread-title turns are ignored.
  • Codex gets its own voice if you also run Claude Code: earpiece voice <id> --agent codex.
  • Too chatty? Codex sends no turn length, so every turn is announced by default. Set agents.codex.minTurnSeconds in ~/.earpiece/config.json, or use earpiece quiet.
  • Approvals on the card. With the optional “Answer from the card” setting, Codex permission requests show up in the Mac app’s notch card with Allow and Deny. It uses Codex’s hooks, so trust them once with /hooks.

Install the Mac app and click Connect on the Agents page, or run node bin/earpiece.mjs install from a clone of the repo. Running Claude Code too? See running several coding agents in parallel.