Letting Codex grind through a long refactor or multi step change while you watch YouTube is nice. Realizing 20 minutes later that it was done after 30 seconds is not.
This guide shows how to wire Codex into native desktop notifications on:
- Linux / Unix (notify send)
- Windows (Python + win10toast)
- WSL (Codex in WSL, notifications in Windows via PowerShell)
All three variants use the same basic mechanism: Codex calls a script on every completed turn, passes a JSON payload, and the script triggers a desktop notification.

Where this integrates nicely with your workflow
Once Codex can nudge you through the OS notification system, several patterns open up:
-
Use Codex to perform codebase wide changes while you are reviewing merge requests in the browser.
-
Let Codex run integration tests, fix failures and report back with “ready to review” notifications.
-
Combine it with a task manager like Kanman to track larger refactors:
- Create a Kanman task for “Migrate FooService to new API”.
- Let Codex execute the mechanical steps.
- Each completed step triggers a notification, and you can tick off subtasks in Kanman as you go.
The notification bridge solves the attention problem: Codex does the heavy lifting, your desktop tells you when it is time to come back and make the next decision.
How Codex notifications work
Codex reads a config.toml file in ~/.codex (Linux / WSL) or C:\Users\<user>\.codex (Windows).
The Codex docs on notifications explain the JSON payload and the notify setting, but they stop at the interface: you still have to supply an OS-specific script, and some setups (WSL, minimal Linux desktops) do not ship anything that can show a toast out of the box.
There is a single key that matters here:
notify = ["command", "arg1", "arg2"]Codex runs this command whenever it wants to raise a notification and appends one final argument: a JSON string describing the event.
Typical payload:
{
"type": "agent-turn-complete",
"last-assistant-message": "I have finished applying the changes.",
"input-messages": [
"Refactor the FooService to use dependency injection.",
"Update all call sites accordingly."
]
}Important points:
typeisagent-turn-completewhen Codex finishes a turn and is waiting on you again.last-assistant-messageis the last message Codex produced.input-messagesis what you asked it to do.
In realtiy, Codex injects a lot of context into input-messages (open tabs, working directory, active file, etc.) and often uses Markdown lists in last-assistant-message:
{
"type": "agent-turn-complete",
"thread-id": "019aa2d1-f501-7760-9354-afb6d88317e9",
"turn-id": "0",
"cwd": "/code/project",
"input-messages": [
"# Context from my IDE setup:\n\n## Active file: /home/<user>/.codex/config.toml\n\n## Open tabs:\n- config.toml: /home/<user>/.codex/config.toml\n- blog.html: themes/kanman/layouts/partials/sections/blog.html\n- hero.yaml: data/home/de/hero.yaml\n- blog.yaml: data/home/de/blog.yaml\n- styles.css: themes/kanman/assets/css/styles.css\n\n## My request for Codex:\nPlease revisit the product box in the sidebar of the blog post layout. Use the text content of the feature section and the hero section to come up with an actual meaningful pitch.\n"
],
"last-assistant-message": "- Refreshed the blog post sidebar pitch in `themes/kanman/layouts/_default/single.html` to mirror the hero/features messaging: project-first, no KPI/gamification noise, drag-and-drop Priorisierung, and no calendar/AI baggage.\n- Swapped CTA copy to “Kanman jetzt nutzen,” added a supporting bullet list with real strengths, and included the lifetime pricing subline from the hero text.\n\nTests not run (copy-only change). Next step: preview the blog post page to ensure the new sidebar layout/line breaks feel right."
}Unfiltered, that would be a wall of text in a toast. The scripts below trim the request down to a short title, pull only the bullet points from the assistant response, and truncate each line so your notification stays readable. You can extend that idea to:
- Strip the “Context from my IDE” block entirely if you do not want path leaks.
- Keep the Markdown bullets (useful to see multiple actions Codex took) or collapse them into a single sentence.
- Route different
typevalues to different notification channels (e.g. errors vs. completions).
You only need to:
-
Implement a small script that:
- reads
argv[1], - parses the JSON,
- decides whether to notify,
- fires a desktop notification.
- reads
-
Point
notifyinconfig.tomlat that script.
The rest of this post is just three concrete scripts for three environments.
Use cases: why bother at all
This wiring is useful whenever Codex is doing something non trivial:
- Large multi file refactors.
- Code generation followed by test runs and fixes.
- Migration work across services or modules.
- Any workflow where Codex says things like “I need your approval to apply these changes”.
Instead of staring at a progress log, you can:
- Switch to another IDE window.
- Work on documentation.
- Or yes, watch YouTube.
The notification brings you back when Codex either finished or needs input again.
Variant 1: Linux / Unix with notify-send
This assumes:
- You run Codex inside a normal Linux desktop session.
notify-sendis available (libnotify-binon Debian / Ubuntu).
Install dependencies
On Debian / Ubuntu:
sudo apt update
sudo apt install -y libnotify-bin python3Notification script
Create ~/.codex/notify.py:
#!/usr/bin/env python3
import json
import re
import subprocess
import sys
def trunc(text: str, limit: int) -> str:
"""Truncate at a word boundary and add ellipsis when needed."""
if len(text) <= limit:
return text
cut = max(0, limit - 3)
head = text[:cut]
head = re.sub(r"\s+\S*$", "", head)
if not head:
head = text[:cut]
return f"{head}..."
def title_from_request(inputs: list[str]) -> str:
"""Use the 'My request for Codex' block as title, else fall back."""
block = "\n".join(inputs or [])
lines = block.splitlines()
capture = False
picked: list[str] = []
for line in lines:
if capture:
picked.append(line)
if line.startswith("## My request for Codex:"):
capture = True
picked.append(line.replace("## My request for Codex:", "", 1).strip())
title = " ".join(" ".join(picked).split()).strip()
return trunc(title or "Codex chat", 120)
def body_from_assistant(text: str) -> str:
"""Prefer Markdown bullets; otherwise condense to one line."""
bullets: list[str] = []
for line in text.splitlines():
if re.match(r"^\s*[-*]\s+", line):
bullets.append("- " + re.sub(r"^\s*[-*]\s+", "", line))
if bullets:
return "\n".join(trunc(b, 80) for b in bullets)
one_line = " ".join(text.split()).strip()
return trunc(one_line or "(no assistant message)", 220)
def main() -> int:
if len(sys.argv) < 2:
return 0
try:
payload = json.loads(sys.argv[1])
except json.JSONDecodeError:
return 0
if payload.get("type") != "agent-turn-complete":
return 0
assistant = (
payload.get("last-assistant-message")
or payload.get("last_assistant_message")
or ""
)
title = title_from_request(payload.get("input-messages") or [])
message = body_from_assistant(assistant)
try:
subprocess.run(["notify-send", title, message], check=False)
except Exception:
# Never break Codex on notification failure
pass
return 0
if __name__ == "__main__":
raise SystemExit(main())What it does (same flow as the WSL and Windows variants below):
- Drops any payload that is not
agent-turn-complete. - Builds the title from the “My request for Codex” section (or falls back to “Codex chat”) and truncates it.
- Pulls Markdown bullets from the assistant reply for the body (else uses a single condensed line), truncating each line.
- Sends the cleaned title/body to
notify-sendand swallows errors.
Make it executable:
chmod +x ~/.codex/notify.pyCodex config on Linux
Edit ~/.codex/config.toml and make sure notify is a top level key near the top of the file:
model = "gpt-5.1-codex-max"
notify = ["python3", "/home/your-user/.codex/notify.py"]
[history]
persistence = "save-all"
[tui]
notifications = ["agent-turn-complete"]Two critical details:
- Use your actual home path.
notifymust be a root key, not inside[tui]or any other table.
Quick end to end test
In a terminal:
python3 ~/.codex/notify.py \
'{"type":"agent-turn-complete","last-assistant-message":"Linux notification test","input-messages":["Example task"]}'You should see a native notification. If that works, start Codex, give it a non trivial task, switch away, and wait for the toast.
Variant 2: Windows notifications with win10toast
On Windows, the easiest path is Python plus the win10toast library. No PowerShell modules, no registry hacks.
This variant assumes:
- Codex runs on Windows, not inside WSL.
- Your
config.tomllives inC:\Users\<user>\.codex\config.toml.
Install Python and win10toast
Install Python if you have not already, then in a terminal or PowerShell:
py -m pip install win10toast(or python -m pip install win10toast depending on your setup).
Notification script on Windows
Create this file:
C:\Users\<user>\.codex\notify.py
#!/usr/bin/env python
import json
import re
import sys
from win10toast import ToastNotifier
def trunc(text: str, limit: int) -> str:
"""Truncate at a word boundary and add ellipsis when needed."""
if len(text) <= limit:
return text
cut = max(0, limit - 3)
head = text[:cut]
head = re.sub(r"\s+\S*$", "", head)
if not head:
head = text[:cut]
return f"{head}..."
def title_from_request(inputs: list[str]) -> str:
"""Use the 'My request for Codex' block as title, else fall back."""
block = "\n".join(inputs or [])
lines = block.splitlines()
capture = False
picked: list[str] = []
for line in lines:
if capture:
picked.append(line)
if line.startswith("## My request for Codex:"):
capture = True
picked.append(line.replace("## My request for Codex:", "", 1).strip())
title = " ".join(" ".join(picked).split()).strip()
return trunc(title or "Codex chat", 120)
def body_from_assistant(text: str) -> str:
"""Prefer Markdown bullets; otherwise condense to one line."""
bullets: list[str] = []
for line in text.splitlines():
if re.match(r"^\s*[-*]\s+", line):
bullets.append("- " + re.sub(r"^\s*[-*]\s+", "", line))
if bullets:
return "\n".join(trunc(b, 80) for b in bullets)
one_line = " ".join(text.split()).strip()
return trunc(one_line or "(no assistant message)", 220)
def main() -> int:
if len(sys.argv) < 2:
return 0
try:
payload = json.loads(sys.argv[1])
except json.JSONDecodeError:
return 0
if payload.get("type") != "agent-turn-complete":
return 0
assistant = (
payload.get("last-assistant-message")
or payload.get("last_assistant_message")
or ""
)
title = title_from_request(payload.get("input-messages") or [])
message = body_from_assistant(assistant)
try:
toaster = ToastNotifier()
toaster.show_toast(
title,
message,
duration=5,
threaded=True,
)
except Exception:
pass
return 0
if __name__ == "__main__":
raise SystemExit(main())Same behavior as the Linux/WSL variants:
- Only reacts to
agent-turn-complete. - Title comes from “My request for Codex,” truncated; fallback is “Codex chat”.
- Body prefers Markdown bullets from the assistant reply; otherwise uses one condensed line, truncating each part.
- Errors are swallowed so Codex keeps running.
Codex config on Windows
Edit C:\Users\<user>\.codex\config.toml:
model = "gpt-5.1-codex-max"
notify = ["py", "C:/Users/your-user/.codex/notify.py"]
[history]
persistence = "save-all"
[tui]
notifications = ["agent-turn-complete"]Adjust the interpreter (py, python, python.exe) and path to match your system.
Test
From PowerShell:
py C:/Users/your-user/.codex/notify.py `
'{"type":"agent-turn-complete","last-assistant-message":"Windows notification test","input-messages":["Example task"]}'If you see a Windows toast, Codex can now ping you whenever a turn completes.
Variant 3: Codex in WSL, notifications in Windows
This is the interesting one.
Scenario:
- You run Codex inside WSL because your toolchain lives there.
- WSL has no GUI or notification daemon.
- You still want native Windows notifications while Codex runs in the background.
The solution:
notifyinconfig.tomlpoints to a Bash script in WSL.- That script receives the JSON payload and forwards it to Windows.
- The Bash script calls
powershell.exedirectly; no extra.ps1file on Windows. - Windows PowerShell uses the BurntToast module to fire a native toast.
You get the best of both worlds: Linux toolchain, Windows notifications.
Install BurntToast on Windows
Open Windows PowerShell (Windows PowerShell 5, not PowerShell 7):
Install-Module -Name BurntToast -Scope CurrentUser -ForceIf NuGet is missing, install it when prompted.
Quick sanity check:
Import-Module BurntToast
New-BurntToastNotification -Text 'Codex', 'Direct BurntToast test'You should see a toast.
Bash wrapper in WSL (calls PowerShell directly)
Install jq if missing:
sudo apt-get install -y jqCreate ~/.codex/notify.sh:
#!/usr/bin/env bash
set -euo pipefail
payload="${1:-}"
type=$(jq -r '.type // empty' <<<"$payload" 2>/dev/null)
[ "$type" != "agent-turn-complete" ] && exit 0
# truncate to max chars, cut back to last full word, add "..." if truncated
trunc() {
local s="$1" n="$2"
[ ${#s} -le $n ] && { printf '%s' "$s"; return; }
local cut=$((n-3))
local head
head=$(printf '%s' "${s:0:$cut}" | sed -E 's/[[:space:]]+[^[:space:]]*$//')
[ -z "$head" ] && head="${s:0:$cut}"
printf '%s...' "$head"
}
# ---- title: extract after "## My request for Codex:"
title=$(jq -r '."input-messages"[]? // ""' <<<"$payload" 2>/dev/null \
| awk 'f{print} /^## My request for Codex:/{f=1; sub(/^## My request for Codex:[[:space:]]*/,""); print}' \
| tr '\n' ' ' | sed -E 's/[[:space:]]+/ /g; s/^[[:space:]]+|[[:space:]]+$//g')
[ -z "$title" ] && title="Codex chat"
title=$(trunc "$title" 120)
# ---- body: bullets if present, else full assistant msg
assistant=$(jq -r '."last-assistant-message" // .last_assistant_message // ""' <<<"$payload" 2>/dev/null)
bullets=$(printf '%s\n' "$assistant" | grep -E '^[[:space:]]*[-*][[:space:]]+' || true)
if [ -n "$bullets" ]; then
message=$(printf '%s\n' "$bullets" \
| sed -E 's/^[[:space:]]*[-*][[:space:]]+/- /' \
| while IFS= read -r l; do trunc "$l" 80; echo; done)
else
one_line=$(printf '%s' "$assistant" | tr '\n' ' ' | sed -E 's/[[:space:]]+/ /g; s/^[[:space:]]+|[[:space:]]+$//g')
message=$(trunc "${one_line:-"(no assistant message)"}" 220)
fi
title_esc=${title//\'/\'\'}
message_esc=${message//\'/\'\'}
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass \
-Command "Import-Module BurntToast; New-BurntToastNotification -Text '$title_esc', '$message_esc' -AppLogo '\\\\wsl.localhost\\Ubuntu-22.04\\home\\<your-user>\\.codex\\codex-logo.png'"What happens in this Bash wrapper:
- Exits unless the payload type is
agent-turn-complete. - Pulls the title from the “My request for Codex” section, trims whitespace, and truncates to 120 chars at word boundaries (fallback: “Codex chat”).
- Prefers Markdown bullets from the assistant reply for the body (each truncated to 80 chars); if none exist, condenses the entire reply to 220 chars.
- Escapes single quotes for PowerShell and calls
New-BurntToastNotificationdirectly from WSL, including an icon (adjust the UNC path to your distro/user or drop-AppLogo).
Make it executable:
chmod +x ~/.codex/notify.shAdjust the UNC path inside -AppLogo to match your distro and user. If you do not want an icon, drop that flag.
Codex config in WSL
Edit /home/your-user/.codex/config.toml inside WSL:
model = "gpt-5.1-codex-max"
notify = ["/bin/bash", "/home/your-user/.codex/notify.sh"]
[history]
persistence = "save-all"
[tui]
notifications = ["agent-turn-complete"]Make sure:
- This
config.tomlis the one the WSL Codex installation uses. notifyis at the top level, before any[table].
Test from WSL
Run:
~/.codex/notify.sh \
'{"type":"agent-turn-complete","last-assistant-message":"WSL test notification","input-messages":["Example task in WSL"]}'If you see a Windows toast, the bridge works.
Now start Codex from WSL (or from VS Code attached to WSL), queue a longer task, switch away, and wait for the notification.
Optional: use an icon
Point the -AppLogo path in ~/.codex/notify.sh to a file Windows can read. Use a UNC path into WSL (\\\\wsl.localhost\\<distro>\\home\\<user>\\.codex\\codex-logo.png) or a straight Windows path (C:\\Users\\your-user\\Pictures\\codex-logo.png). Remove the flag entirely if you want a plain toast.
The rest of the WSL setup stays unchanged.
Summary
The core ideas are simple:
- Codex already knows how to call an external command on each completed turn.
- Every desktop platform has a way to show notifications from scripts.
- Gluing those together is a matter of a few lines of Python, Bash or PowerShell.
Linux users wire notify to a Python script that wraps notify-send.
Windows users point notify at a small Python script using win10toast.
WSL users run a single Bash wrapper that filters the JSON and calls powershell.exe with BurntToast directly - no extra .ps1 file.
Once set up, Codex becomes something you can safely leave running in the background, while focusing on the next item on your Kanman board instead of staring at a log window.
Marco Kerwitz
Founder of kanman.de