Skip to main content
All posts
Developers5 min read

Get a Phone Alert When a Codex Turn Finishes

Connect Codex’s turn-complete notifier to a PingRoom webhook with fixed lock-screen copy, bounded delivery, and no prompt or response text.

Two PingRoom avatars leave a desk as Pingo watches a brass turn token beside an open laptop and a face-down phone.

To get a phone alert when a Codex CLI turn finishes, point Codex’s user-level notify setting at a small local program. The program receives one JSON event after the turn, ignores the prompt and response text, and sends fixed copy to a PingRoom incoming webhook.

The boundary matters: turn complete means Codex finished processing one user submission. It does not mean the whole project is complete, the result is correct, or an approval is waiting. A longer task with several follow-up turns can produce several alerts.

First decide whether your terminal already does enough

Codex has two notification paths. Its built-in TUI notifications can alert the current computer. The external notify setting runs your own program, which is the useful path when the destination is a phone or a shared Room.

Use the lightest option that fits:

You needStart with
A bell or desktop alert on the computer running CodexCodex TUI notifications
A phone alert after a completed Codex turnnotify plus a PingRoom incoming webhook
An alert when Codex asks for approvalA separate approval-aware Codex notification or hook; the recipe below does not cover it
The exit result of any shell command, with or without CodexThe generic long-running command wrapper

The Codex configuration reference says notification configuration belongs in the user-level file, normally ~/.codex/config.toml. A project’s .codex/config.toml cannot override notify.

Create one private Room connection

In PingRoom, create a private Room for the people who should review the result. Open Connections, add an Incoming webhook, and copy its URL. Incoming webhooks require PingRoom Pro.

Treat the full URL as a secret. Do not put it in the Python file, config.toml, source control, screenshots, or logs. Export it in the environment that launches Codex, or load it with the secret manager you already use:

export PINGROOM_WEBHOOK_URL='https://api.pingroom.io/api/webhooks/...'
codex

The incoming-webhook reference has the current request fields and limits. If you have not sent a Ping programmatically before, the webhook-to-push walkthrough covers Room setup, secret handling, and the recipient outcome.

Install a privacy-minimal notifier

Save this as ~/.codex/codex-pingroom-notify.py and make it executable. It uses only Python’s standard library.

#!/usr/bin/env python3

import hashlib
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None


def main() -> int:
    if len(sys.argv) != 2:
        print("expected one Codex notification JSON argument", file=sys.stderr)
        return 1

    try:
        event = json.loads(sys.argv[1])
    except (json.JSONDecodeError, TypeError):
        print("ignored malformed Codex notification", file=sys.stderr)
        return 1

    if event.get("type") != "agent-turn-complete":
        return 0

    thread_id = event.get("thread-id")
    turn_id = event.get("turn-id")
    if not isinstance(thread_id, str) or not thread_id:
        print("missing Codex thread-id", file=sys.stderr)
        return 1
    if not isinstance(turn_id, str) or not turn_id:
        print("missing Codex turn-id", file=sys.stderr)
        return 1

    webhook_url = os.environ.get("PINGROOM_WEBHOOK_URL", "")
    parsed = urllib.parse.urlparse(webhook_url)
    loopback_http = parsed.scheme == "http" and parsed.hostname in {
        "127.0.0.1",
        "localhost",
        "::1",
    }
    if parsed.scheme != "https" and not loopback_http:
        print("PINGROOM_WEBHOOK_URL must use HTTPS", file=sys.stderr)
        return 1

    identity = f"{thread_id}:{turn_id}".encode("utf-8")
    event_key = "codex-turn-" + hashlib.sha256(identity).hexdigest()[:32]
    body = json.dumps(
        {
            "title": "Codex turn finished",
            "message": "Your Codex turn is ready to review.",
            "correlation_id": event_key,
        },
        separators=(",", ":"),
    ).encode("utf-8")

    request = urllib.request.Request(
        webhook_url,
        data=body,
        method="POST",
        headers={
            "Content-Type": "application/json",
            "Idempotency-Key": event_key,
            "User-Agent": "codex-pingroom-notify/1.0",
        },
    )

    try:
        with urllib.request.build_opener(NoRedirect).open(request, timeout=10) as response:
            if response.status != 200:
                print(f"PingRoom returned HTTP {response.status}", file=sys.stderr)
                return 1
    except (urllib.error.URLError, TimeoutError) as error:
        print(f"PingRoom notification failed: {error}", file=sys.stderr)
        return 1

    return 0


if __name__ == "__main__":
    raise SystemExit(main())
chmod 700 ~/.codex/codex-pingroom-notify.py

The official Codex source describes the exact boundary: after each completed turn, Codex launches the configured program and appends one JSON event argument. The event also contains fields such as the working directory, input messages, and final assistant message. This adapter deliberately reads none of that content.

Instead, it hashes the thread and turn identifiers into a non-secret event key. Replaying the same event produces the same PingRoom body and Idempotency-Key; a new turn produces a new key. The request refuses non-HTTPS remote URLs, does not follow redirects, and stops waiting after ten seconds.

Two PingRoom avatars compare one accepted turn token with an identical retry held at a brass gate while Pingo watches the duplicate.

These AI-generated editorial illustrations feature approved PingRoom avatars and Pingo the Pigeon, PingRoom’s fictional Chief Marketing Officer. They are not Codex or PingRoom screenshots and do not depict customers.

Pingo would probably stand beside the laptop until the token lands. Humans have better things to do. In this example, the Room gets one restrained Codex turn finished Ping, and the person decides when to return and review the actual result in Codex.

Point Codex at the program

Add this top-level line near the beginning of ~/.codex/config.toml, before any [section] whose table could absorb it. Use the absolute path on your machine:

notify = ["python3", "/Users/you/.codex/codex-pingroom-notify.py"]

Start Codex from an environment that has PINGROOM_WEBHOOK_URL, then run a harmless turn such as asking it to summarize a small public file. When the turn ends, confirm all three surfaces:

  1. Codex shows its final response.
  2. The private Room receives one Codex turn finished Ping.
  3. The phone preview contains no prompt, response, file path, repository name, or customer data.

Then send one follow-up prompt in the same Codex session. It should create a second alert because it is a second turn. This is expected; the notifier does not attempt to infer whether a multi-turn task is “really done.”

What this recipe proves—and what it does not

The adapter was checked against a local stateful HTTP mock with synthetic Codex events. The checks covered accepted delivery, exact replay, a new turn identity, unsupported event types, malformed JSON, missing identifiers, missing configuration, insecure remote HTTP, server rejection, headers, request body, and exclusion of prompt, response, path, and sample secret text. No live Codex account, PingRoom Room, phone, or push receipt was used in that test.

The alert means the notifier observed agent-turn-complete and PingRoom accepted the webhook request. It does not certify the work, replace code review, keep Codex running through a disconnected host, or report approval requests. Phone delivery can still be delayed or suppressed by network conditions, Room settings, operating-system notification policy, or Focus modes.

If a fixed completion signal is the boundary you need, create the private Room, add one incoming webhook, and test the smallest harmless Codex turn before trusting the workflow with real work.

PingRoom

Short-form communication.

View all posts