# 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.

- Published: 2026-10-26
- Author: PingRoom
- Category: Developers
- Topics: codex cli notifications, Codex turn complete notification, Codex phone notification
- Canonical: https://pingroom.io/blog/codex-turn-complete-phone-notifications
- Cover image: https://pingroom.io/blog/codex-turn-complete-phone-notifications-cover.webp
- Cover description: 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](https://learn.chatgpt.com/docs/config-file/config-advanced#notifications) 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 need                                                    | Start with                                                                                  |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| A bell or desktop alert on the computer running Codex       | Codex TUI notifications                                                                     |
| A phone alert after a completed Codex turn                  | `notify` plus a PingRoom incoming webhook                                                   |
| An alert when Codex asks for approval                       | A separate approval-aware Codex notification or hook; the recipe below does not cover it    |
| The exit result of any shell command, with or without Codex | [The generic long-running command wrapper](/blog/notify-when-long-running-command-finishes) |

The [Codex configuration reference](https://learn.chatgpt.com/docs/config-file/config-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:

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

The [incoming-webhook reference](/documents/webhooks) has the current request fields and limits. If you have not sent a Ping programmatically before, the [webhook-to-push walkthrough](/blog/webhook-to-push-notification) 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.

```python
#!/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())
```

```bash
chmod 700 ~/.codex/codex-pingroom-notify.py
```

The official [Codex source describes the exact boundary](https://github.com/openai/codex/blob/main/codex-rs/core/src/config/mod.rs): 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.](/blog/codex-turn-complete-phone-notifications-1.webp)

_These AI-generated editorial illustrations feature approved PingRoom avatars and [Pingo the Pigeon](/Pingo), 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:

```toml
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](/app/rooms), add one incoming webhook, and test the smallest harmless Codex turn before trusting the workflow with real work.
