Skip to content

lucianlamp/codex-monitor

Repository files navigation

codex-monitor

codex-monitor — Local-first monitor for Codex app-server events

codex-monitor is a local-first monitor for delivering external events into the Codex App / Codex app-server control plane.

The preferred short command is cdxm. It is a compatibility launcher for the single native codex-monitor executable, not a second compiled binary.

Install for daily use

macOS / Linux

One-liner install:

curl -fsSL https://raw.githubusercontent.com/lucianlamp/codex-monitor/main/install.sh | bash

On macOS arm64 and Intel, the installer downloads the matching prebuilt binary from GitHub Releases and verifies its SHA256 checksum — no Rust or Cargo toolchain is needed. On other platforms (e.g. Linux), or when --build-from-source is passed, it falls back to a source build, which requires Rust and Cargo.

The installer asks before each user-visible step:

  • install native codex-monitor and the cdxm compatibility launcher under $HOME/.codex-monitor/bin
  • install the Codex skill under $HOME/.codex/skills/codex-monitor
  • optionally install a Codex CLI shim at $HOME/.agents/bin/codex
  • add $HOME/.codex-monitor/bin and $HOME/.agents/bin to PATH in ~/.zshrc

The Codex shim prompt defaults to no. If $HOME/.agents/bin/codex already exists, the installer leaves it untouched unless you explicitly request the shim (--install-shim); in that case it reports the detected kind, keeps a backup of the previous entrypoint (codex.bak-<timestamp>), and installs the codex-monitor shim.

Pass --skip-build to register skills and shim without installing binaries.

From this repository, the equivalent local install is:

./install.sh

Windows native PowerShell

Windows is supported with a native PowerShell installer (no WSL). The codex-monitor.exe is native; cdxm.cmd is a compatibility launcher. The optional Codex shim runs the same bash shim used on macOS/Linux through Git Bash, so installing that shim requires Git Bash:

iwr https://raw.githubusercontent.com/lucianlamp/codex-monitor/main/install.ps1 -UseBasicParsing | iex

On Windows x86_64, the installer downloads a prebuilt binary from GitHub Releases and verifies its SHA256 checksum — no Rust, Cargo, or MSVC Build Tools are needed for the prebuilt path. Passing -BuildFromSource falls back to a source build, which still requires the Rust MSVC toolchain plus MSVC Build Tools.

From this repository, the equivalent local install is:

powershell -ExecutionPolicy Bypass -File .\install.ps1

The Windows installer asks before each user-visible step:

  • install native codex-monitor.exe under %USERPROFILE%\.codex-monitor\bin
  • install the cdxm.cmd compatibility launcher under %USERPROFILE%\.agents\bin
  • install the Codex skill under %USERPROFILE%\.codex\skills\codex-monitor
  • optionally install a Codex CLI shim at %USERPROFILE%\.agents\bin\codex.cmd (a thin launcher that runs the shared bash shim through Git Bash)
  • add %USERPROFILE%\.codex-monitor\bin and %USERPROFILE%\.agents\bin to the user PATH

The Codex shim prompt defaults to no. If codex.cmd already exists, the installer leaves it untouched unless you explicitly request the shim (-InstallShim); in that case it reports the detected kind, keeps a backup of the previous entrypoint (codex.cmd.bak-<timestamp>), and installs the codex-monitor shim.

Pass -SkipBuild to register skills and shim without installing binaries.

For a non-interactive install with the shim:

powershell -ExecutionPolicy Bypass -File .\install.ps1 -Yes -InstallShim

For a local source install without touching the Codex shim:

powershell -ExecutionPolicy Bypass -File .\install.ps1 -Source . -NoShim

Keep Windows Codex App on its OpenAI-signed native codex.exe. Leave CODEX_CLI_PATH unset or point it directly at that signed App-managed executable; do not use an external App launcher. In Codex App, the installed skill provides three explicit receive modes:

$codex-monitor            arm a session Stop hook after every completed turn
$codex-monitor heartbeat  create or update a one-minute App heartbeat
$codex-monitor off        disable this session's Stop hook and heartbeat

The default mode installs one dormant global Stop hook and enables it with a marker scoped to the current App session. After every completed turn, its internal foreground helper calls the installed agmsg inbox.sh until a message arrives. Empty polls stay local and do not start model turns; a real message is saved as a session-private pending delivery before it is returned as a Stop continuation prompt. The pending delivery is acknowledged only when that continuation finishes and reaches the next Stop. If a user input interrupts the continuation before it appears, the next ordinary Stop replays the same pending message instead of consuming another inbox row. Delivery is therefore at-least-once: an ambiguous interruption can repeat a message, but cannot silently lose it. The marker automatically re-arms for the next completed turn, and $codex-monitor off removes both the marker and its pending delivery. The first hook definition or a changed definition must be trusted once in Codex App Settings > Hooks: review the handler whose status is Waiting for agmsg via codex-monitor, then choose Trust. The /hooks command is not required. Heartbeat mode remains an explicit fallback. Neither mode starts a watcher or changes the App executable.

In Codex CLI, $codex-monitor has different semantics: it runs cdxm-agmsg-apply.sh and applies a durable receiver for the current session. On Windows that receiver is a detached background monitor watch; on macOS it is a loaded LaunchAgent. The App-only Stop hook and foreground inbox helper must not be used for the CLI shortcut.

Run updates from any directory on Windows or macOS:

codex-monitor update

On macOS arm64 and Intel, the command downloads the matching tar.gz release, requires its SHA-256 checksum, and atomically updates one native executable at $HOME/.codex-monitor/bin/codex-monitor. It regenerates cdxm as a POSIX launcher, migrates owned com.local.codex-monitor.agmsg.* plists, and reloads each exact LaunchAgent that was already loaded. A reload failure restores the original plists and loaded arguments before returning an error. Fixed legacy native copies under $HOME/.cargo/bin are removed only after all selected agents verify the canonical launcher. Linux self-update is not supported; use install.sh there.

On Windows, the command verifies the latest release checksum and updates the single native codex-monitor.exe. It refreshes cdxm.cmd, preserves an explicit native or otherwise unowned CODEX_CLI_PATH, and restores a proven-owned legacy bridge to its saved environment. A running legacy cdxm.exe remains untouched until a later update after its consumer exits. The Windows updater never stops App, a watcher, or a CLI process.

On Windows, --target app is intentionally unavailable because native App has no safe external injection endpoint. Use the three skill shortcuts for App delivery. The CLI shim and its app-server endpoint remain independent.

For development-only binary refresh:

cargo install --path . --bin codex-monitor --force --debug

This installs only the native executable into Cargo's bin directory, usually:

$HOME/.cargo/bin/codex-monitor

Confirm it is available:

command -v codex-monitor
codex-monitor --help

If command -v codex-monitor prints nothing, add Cargo's bin directory to your shell PATH. Use install.sh or install.ps1 instead when the cdxm compatibility command is desired.

export PATH="$HOME/.cargo/bin:$PATH"

After changing this repository's source code, rerun the install command to refresh the installed binaries.

For an optimized binary, omit --debug. The optimized install can take much longer on macOS because the bundled SQLite dependency is compiled with release optimizations.

Codex CLI entrypoint

For daily Codex CLI monitoring, start interactive CLI sessions through a shim that makes them app-server-bound. The common local setup is the agmsg-compatible Codex shim at:

$HOME/.agents/bin/codex

Put that directory before the real Codex binary on PATH:

export PATH="$HOME/.agents/bin:$PATH"
type -a codex

The first codex should be a shim. It may be an existing agmsg shim or the CDXM shim installed by install.sh; the important property for codex-monitor is that interactive codex launches are routed through app-server/--remote. Without a shim or an explicit codex --remote ..., a plain real-Codex TUI process is not a reliable live injection target for cdxm.

Daily flow

List auto-discovered Codex App / Codex CLI app-server endpoints:

cdxm targets

For an already-loaded thread, cdxm can usually attach automatically by cwd. Query the loaded thread for a working directory:

cdxm threads --cwd /path/to/project

Start foreground monitor delivery into the loaded thread for that cwd, using agmsg as the input adapter:

cdxm monitor watch agmsg --team <team> --name <agent> --cwd /path/to/project

The cdxm agmsg watch ... command remains as a source-specific shortcut for the same adapter.

Stop the watcher with Ctrl-C.

Commands

cdxm targets
cdxm threads --cwd <path>
cdxm send --cwd <path> --text <msg>
cdxm send --cwd <path> --text <msg> --mode start --wait
cdxm send --thread <id> --mode steer --turn <turn-id> --text <msg>
cdxm monitor watch agmsg --team <team> --name <agent> --cwd <path> --dry-run
cdxm monitor watch agmsg --team <team> --name <agent> --cwd <path>
cdxm agmsg doctor --team <team> --name <agent> --cwd <path>
cdxm agmsg watch --team <team> --name <agent> --cwd <path> --dry-run
cdxm agmsg watch --team <team> --name <agent> --cwd <path>
cdxm agmsg launch-agent install --team <team> --name <agent> --cwd <path>
cdxm agmsg launch-agent status --team <team> --name <agent>
cdxm remote doctor
cdxm remote connect --max-messages 1

Debug commands such as loaded, steer, and low-level remote ... recovery commands still exist. Most are hidden from help and are not the daily command surface; remote connect is visible as an explicit remote-control client probe.

Targets

Default target is auto.

auto discovers live Codex endpoints from:

  • CDXM_ENDPOINT, CODEX_MONITOR_ENDPOINT, or CODEX_APP_SERVER_ENDPOINT
  • the Codex App control socket
  • live codex --remote ..., codex app-server --listen ..., and agmsg Codex bridge processes

For cwd-based commands, such as threads --cwd, send --cwd, and monitor watch <adapter> --cwd, auto probes candidate endpoints with thread/loaded/list plus thread/list and chooses the live endpoint where a thread for that cwd is already loaded. If no live endpoint has that cwd loaded, commands that need a thread safely fall back to managed mode only when managed can resolve a unique matching thread.

For commands with an explicit --thread, such as send and monitor watch <adapter>, auto probes candidate endpoints with thread/loaded/list and chooses the one where the thread is already loaded. This avoids thread/resume and prevents the resume/fork failure mode.

List candidates:

cdxm targets

If auto is ambiguous, pass --endpoint or --target explicitly.

Hidden diagnostic command loaded is endpoint-scoped. If cdxm loaded is ambiguous because several live endpoints are present, use cdxm targets and rerun with --target app, --target managed, or --endpoint <url>.

Managed mode starts an isolated loopback app-server at ws://127.0.0.1:<port>: most daily commands do not need this mode explicitly.

Existing app-server control daemon attach is explicit:

cdxm --target app threads --cwd <path>
cdxm --target app send --cwd <path> --text <msg>
cdxm --target app remote doctor

On Unix, --target app connects to:

$HOME/.codex/app-server-control/app-server-control.sock

On Windows, --target app returns an error directing App delivery to $codex-monitor Stop hook wait or $codex-monitor heartbeat. Native App does not expose a safe external injection endpoint. Explicit and CLI-managed endpoints remain available.

--endpoint ws://127.0.0.1:<port> connects to an explicit loopback WebSocket. --endpoint unix:///path/to/app-server.sock connects to an explicit Unix WebSocket app-server socket. --endpoint stdio:// starts an isolated stdio app-server.

Existing Thread Send

cdxm send delivers text as app-server user input. The default mode is auto: it reads the loaded thread, uses turn/steer when an active inProgress turn is visible, and otherwise falls back to turn/start.

By default the command returns after the app-server acknowledges the turn/steer or turn/start request. It does not wait for turn/completed, which avoids deadlocks when using cdxm to inject into the same live Codex App thread that is controlling the command.

Use --wait only when the caller really needs synchronous completion:

cdxm --target app send --thread <id> --text <msg> --mode start --wait

Use --mode start to force a new turn. Use --mode steer --turn <turn-id> to force steering into a known active turn:

cdxm --target app send --thread <id> --mode steer --turn <turn-id> --text <msg>

When --target app, --endpoint, or auto-resolved live endpoints are used, send first checks thread/loaded/list and refuses unloaded threads. It still does not call thread/resume. The hidden steer command uses the same loaded-thread guard.

Remote Control Diagnostics

cdxm remote ... talks to the Codex app-server remote-control RPC surface. This is the same local control plane used to expose this computer as a remote-control environment for other Codex clients.

Use remote doctor as the primary visible command. It diagnoses the remote-control surfaces without changing account or device state:

cdxm --target app remote doctor

remote doctor prints tab-separated doctor rows for each independent surface:

  • app-server-status: local app-server remote-control state.
  • app-server-clients: clients returned by local app-server remoteControl/client/list.
  • auth-refresh and auth-file: local ChatGPT auth refresh/read status.
  • backend-clients: clients returned by ChatGPT backend /wham/remote/control/clients.
  • local-enrollment: enrolled controller client and device-key id found in the local Codex global state file.
  • device-key: whether the local native device-key module can read the enrolled public key.
  • device-key-next: repair guidance when the local controller enrollment is stale, missing, mismatched, or unsupported.

The command is intentionally non-destructive. It does not revoke clients, delete enrollments, edit Codex App state, sign out, or reset device keys.

Use remote connect as the explicit probe for making cdxm behave like an enrolled remote-control controller client, similar to the phone-side transport:

cdxm --target app remote connect --max-messages 1

This connects to the remote-control websocket with the local enrolled client and device key, prints the proof algorithm, and optionally prints observed messages. It does not yet open or select a Codex thread by itself. If it fails with device key not found, remote doctor will emit doctor<TAB>device-key-next<TAB>repair-local-controller-enrollment. That state requires Codex App Settings remote-control re-authorization before cdxm can act like the phone; cdxm should not silently recreate controller device keys.

If Codex App settings says it cannot load the device list while a phone can still remote-control this Mac, run remote doctor. The settings UI combines the backend client list with the local app-server client list, so a failure in either read path can break the settings list even when an already-paired phone connection remains usable. A stale or missing local Mac controller key appears as doctor<TAB>device-key<TAB>warn<TAB>...<TAB>unavailable; that explains why this Mac cannot act as a controller client, but it does not by itself prove the phone side is broken.

Hidden advanced recovery commands remain available for explicit troubleshooting: remote status, remote clients, remote monitor, remote enable, remote disable, remote pair-start, remote pair-status, and remote claim.

Monitor Adapters

cdxm monitor is the generic receive surface. A monitor adapter converts an external event stream into BridgeEvent records; the monitor core resolves the loaded Codex thread, sends the event with turn/start or turn/steer, and advances adapter state only after app-server acknowledgement.

The core boundary is:

  • thread detection: find a loaded Codex App / Codex CLI app-server thread by cwd or explicit thread id, without implicitly calling thread/resume
  • source adapter: poll an external event source, expose monotonically ordered BridgeEvent records, and format each event as Codex turn input
  • delivery: send formatted input with the selected auto, start, or steer mode and advance the adapter cursor only after app-server ack

Current adapter:

cdxm monitor watch agmsg --team dev --name sally --cwd /path/to/project

cdxm agmsg watch ... is kept as a source-specific shortcut for the agmsg adapter.

agmsg Adapter

Start agmsg monitor work with a read-only runtime snapshot:

~/.codex/skills/codex-monitor/scripts/cdxm-context.sh /path/to/project <team> <agent>

The agmsg adapter reads the message store directly and does not use Codex shims, PATH replacement, inbox.sh, or watch.sh. CLI sessions still need to be app-server-bound before codex-monitor can inject into them; for daily use, the standard entrypoint is a PATH-first codex shim as described above. The adapter only polls unread inbox rows where read_at IS NULL; previously read history is ignored even when the codex-monitor cursor state is empty. The SQLite adapter is enabled on macOS, Linux, and native Windows builds. On Windows, use the native PowerShell installer and Codex CLI shim for app-server-bound Codex sessions; agmsg watch reads the same SQLite message DB through bundled rusqlite.

Default agmsg DB:

$HOME/.agents/skills/agmsg/db/messages.db

On native Windows, the default resolves through HOME when present, otherwise USERPROFILE:

%USERPROFILE%\.agents\skills\agmsg\db\messages.db

Use the current working directory as the target thread selector:

cdxm monitor watch agmsg --team dev --name sally
cdxm agmsg watch --team dev --name sally

Or pass a cwd explicitly:

cdxm monitor watch agmsg --team dev --name sally --cwd /path/to/project
cdxm agmsg watch --team dev --name sally --cwd /path/to/project

Pin a loaded thread id, or override the agmsg DB:

cdxm monitor watch agmsg --team dev --name sally --thread <loaded-thread-id> --agmsg-db /path/to/messages.db
cdxm agmsg watch --team dev --name sally --thread <loaded-thread-id> --agmsg-db /path/to/messages.db

Before installing or starting a long-running watcher, inspect the full routing surface:

cdxm agmsg doctor --team dev --name sally --cwd /path/to/project

agmsg doctor prints tab-separated doctor rows for discovered targets, matching loaded threads, the agmsg:<team>:<name> saved-state key and id, latest inbox ids, target and same-team LaunchAgents with log mtimes, active agmsg consumer processes, and an explicit ack-vs-visible note.

Preview one delivery plan without sending a turn or marking state:

cdxm monitor watch agmsg --team dev --name sally --cwd /path/to/project --dry-run
cdxm agmsg watch --team dev --name sally --cwd /path/to/project --dry-run

Dry-run delivery rows are source-agnostic. They include source, cursor, event_id, and adapter metadata such as agmsg_id, team, recipient, and sender.

With the default --target auto, agmsg watch first tries to attach to a discovered live endpoint where the cwd or <loaded-thread-id> is already loaded. For a specific Codex App session use --target app; for a specific Codex CLI session use the socket from cdxm targets:

cdxm --endpoint unix:///path/to/codex-app-server.sock agmsg watch --team dev --name sally --thread <loaded-thread-id>

Delivery uses the same non-waiting auto send path as cdxm send: if the loaded thread has an active in-progress turn it calls turn/steer, otherwise it calls turn/start. In both cases the agmsg row is marked seen after the app-server acknowledges the request; agmsg watch does not wait for turn/completed. Use --mode start, --mode steer, or --mode auto to make delivery behavior explicit during diagnosis.

A live watcher keeps the requested logical target and reconnects when endpoint resolution, transport setup, or delivery fails. When an event is pending, --target app and --target auto also re-resolve the logical target before delivery. If the connected endpoint or thread is no longer current, the watcher rejects the old session even when it still responds, reconnects, and retries from the unchanged source cursor. Explicit and managed targets do not add this discovery probe.

App-server ack and codex-monitor state advancement mean the app-server accepted the delivery. They do not prove that the current Codex UI rendered a visible message. In particular, active-turn turn/steer input may reach the model without rendering as a separate App bubble. Treat live smoke as two checks: codex-monitor state/inbox advancement, and model receipt in the intended loaded thread; require a separate visible bubble only when that UI behavior matters.

macOS LaunchAgent

For a durable local monitor bridge, install a per-user LaunchAgent. This writes a plist under $HOME/Library/LaunchAgents and logs under $HOME/Library/Logs/codex-monitor.

Preview the plist:

cdxm agmsg launch-agent print --team dev --name sally --cwd /path/to/project

Install without starting:

cdxm agmsg launch-agent install --team dev --name sally --cwd /path/to/project

Install and start immediately:

cdxm agmsg launch-agent install --team dev --name sally --cwd /path/to/project --force --load

Check or remove it:

cdxm agmsg launch-agent status --team dev --name sally
cdxm agmsg launch-agent uninstall --team dev --name sally

The LaunchAgent label is stable per team/name, for example com.local.codex-monitor.agmsg.dev.sally. Reinstall with --force to change cwd, endpoint, or DB settings. If multiple agents share a team, choose the exact --name; do not install a second watcher for a role that already has another bridge delivering the same inbox.

launch-agent status includes stdout/stderr log paths, mtimes, desired thread, active thread, and args_match. This makes stale launchd ProgramArguments visible instead of hiding them behind installed=true loaded=true.

codex-monitor update and a real Darwin install.sh binary install converge existing owned LaunchAgents on $HOME/.codex-monitor/bin/cdxm. Migration preserves cwd, team/name, thread pin, endpoint, mode, DB, logs, and all arguments after the executable path; it reloads only the exact matching labels.

Troubleshooting

Codex App: failed to spawn code-mode host (Windows)

If the Codex / ChatGPT App fails to run tools with an error like:

failed to spawn code-mode host
...\OpenAI\Codex\bin\<hash>\codex-code-mode-host.exe
指定されたファイルが見つかりません。 (os error 2)

and the official App Repair / reinstall does not fix it, the cause is a stale CODEX_CLI_PATH user environment variable left pointing at an old App build hash (...\OpenAI\Codex\bin\<oldhash>\codex.exe). The App resolves its code-mode host next to that path, but the old build no longer exists after the App updated its hash, so the launch fails. Repair never touches user environment variables, so it cannot clear this.

Early codex-monitor versions set CODEX_CLI_PATH as part of the (now removed) App bridge and could leave it stranded. Updating to v0.2.0 or later (irm https://raw.githubusercontent.com/lucianlamp/codex-monitor/main/install.ps1 | iex) reclaims a stranded value automatically when the ownership backup still exists. If it does not, remove the variable manually:

# 1. Inspect the current value (confirm it points into OpenAI\Codex\bin)
[Environment]::GetEnvironmentVariable('CODEX_CLI_PATH','User')

# 2. Remove it (restores the pre-codex-monitor state)
[Environment]::SetEnvironmentVariable('CODEX_CLI_PATH', $null, 'User')

Then fully quit the Codex / ChatGPT App from the tray and relaunch it — the old value is inherited by already-running processes, so a restart is required. Verify from a new Codex task:

powershell -NoProfile -Command "Write-Output ('codex-host-ok ' + [DateTime]::Now.ToString('HH:mm:ss'))"

This only removes a value codex-monitor itself set; a CODEX_CLI_PATH you rely on for your own tooling should be repointed rather than deleted.

Safety

  • codex-monitor never auto-approves Codex app-server requests.
  • codex-monitor refuses non-loopback WebSocket endpoints in the MVP.
  • thread/inject_items is not the default delivery path.
  • --target app does not start, stop, or replace the app-server control daemon.
  • live-target send, its hidden steer command, and monitor watch <adapter> refuse to call thread/resume; the target thread must already be loaded in that app-server.

License

Licensed under the MIT License.

Releases

Packages

Contributors

Languages