CLI Lifecycle
These commands install, run, inspect, repair, and update the local opencodex proxy and its Codex integration.
Discover before starting services
Section titled “Discover before starting services”ocx help, ocx help <family> and ocx help <family> <leaf> are offline.
Use ocx help --all when the compact root is insufficient. Help, local config
validation and local Lab inspection do not require starting a proxy.
Before a live management workflow, run ocx ready --json and ocx status --json.
Readiness distinguishes pending, failed and unreachable; only ready: true
confirms readiness. Check the target and versionSkew.relation: unknown is not
proof of matching versions. A version mismatch calls for resolving the intended
installation, not repeatedly trying a newer flag against an older server.
Output and exit conventions are command-specific. ready and resolve reject
invalid arguments with 64, while doctor --json is unsupported and exits 2.
A successful local save or accepted restart request does not prove convergence;
read the command receipt and re-check status after the requested operation.
When live target discovery finds no proxy, policy/catalog commands, key-scoped usage and Aside profile sync report: “Proxy is not running. Start the intended proxy with: ocx start. No request was sent.” A management request that was sent can still have an uncertain write outcome; read back from the intended target before retrying. Legacy client-integration refusals add fixed recovery guidance keyed on the refusal reason (never the writer’s own text); backup and residual-recovery notices remain available.
ocx system update run --yes prints the accepted job ID and observed job state. Follow it
with ocx system update status <job-id>; acceptance does not establish successful installation
or restart. --json retains the complete server response.
ocx init · ocx setup
Section titled “ocx init · ocx setup”Interactive setup wizard (setup is an alias of init). Prompts for a provider (preset or custom),
API key (literal or ${ENV}), default model, and proxy port; saves ~/.opencodex/config.json;
optionally injects the proxy into $CODEX_HOME/config.toml (default ~/.codex/config.toml); and
optionally installs the Codex autostart shim.
Proxy lifecycle
Section titled “Proxy lifecycle”ocx start [--port <port>] [--socks5 [host:port] | --socks5-off]
Section titled “ocx start [--port <port>] [--socks5 [host:port] | --socks5-off]”Start the proxy server (preferred port 10100). It writes PID/runtime-port state and refuses to
start a second live instance. When the preferred port is occupied, start asks the holder who it
is and stops either way: it refuses outright when an opencodex answers there, and reports an
unidentified holder otherwise. It never moves the listener to another port on its own, because that
would leave the first proxy running and re-point Codex at the second. An explicit different
--port is still refused when the live proxy shares this OPENCODEX_HOME, because observe-only
and enforced spend accounting both write the same journal. Use a separate OPENCODEX_HOME for an
independent sibling; port: 0 only asks the OS for that instance’s port and does not separate its
state. On start it syncs each provider’s models into Codex’s catalog. On shutdown it restores
native Codex — unless it was launched as a managed service (OCX_SERVICE=1). A sibling started
beside a running proxy does neither: it serves direct requests on its own port only, and Codex,
Grok and Claude stay pointed at the proxy that was already running. With a separate
OPENCODEX_HOME, startup checks the default home’s runtime record and managed Grok and
Codex loopback destinations for a live opencodex owner before syncing. A custom home with
no live owner still syncs normally; explicit ocx sync and ocx grok apply remain available.
Stopping that sibling with ocx stop or a signal leaves their configuration alone as well. While it runs, the proxy also
keeps Codex pointed at itself: when the opencodex routing in ~/.codex/config.toml names another
local port where no opencodex has answered for about 20 seconds (an instance that re-pointed it and
then died, for example), the proxy re-points Codex at its own port and prints one warning. Codex
threads opened in the meantime keep the dead address until you reopen them.
--socks5 (default 127.0.0.1:10808) saves config.proxy as a SOCKS5 URL and routes outbound
HTTP(S) through a real SOCKS5 tunnel. --socks5-off clears only that saved SOCKS5 proxy; it
does not remove an HTTP proxy. The value survives ocx update because it lives in config, not in
the installed package. A proxy username and password may be included in the URL, but startup
logs redact them.
ocx startocx start --port 8080ocx start --port 10100 --socks5ocx start --socks5-offocx stop [--json]
Section titled “ocx stop [--json]”Stop the running proxy (by PID), remove the PID file, and restore native Codex for standalone owners. If a managed
background service is installed, ocx stop also stops it first so it cannot respawn the proxy.
On a connected hub client, ocx stop leaves Codex routing, its
model catalog and the Grok config as they are, including after a successful API stop: the connection
owns them, and ocx start serves them again without re-injecting routing. Use ocx disconnect to hand Codex back to its own account. If Codex on a connected
client already reads native routing, ocx status names ocx sync --restart-codex to point it back.
An explicit stop on a Remote Link Child restores native Codex routing, catalog and backed-up
resume-history metadata and removes the managed Grok configuration when shared restoration succeeds.
The saved Home connection is kept; ocx disconnect removes it. To use Home from Codex again,
start the Child with ocx start (or ocx service start for an installed service), then run
ocx sync --restart-codex. Starting alone does not reapply routing; sync does not recreate the Grok block.
If connection ownership cannot be confirmed before service stop, the stop API returns HTTP 409 with
sharedTeardown: "refused" and keeps the proxy and service manager running. Let any pending
connect or disconnect finish, then retry.
A connected stop verifies the listener against the protected local runtime record before changing
its service. Both client-role listeners and ordinary proxies must prove that identity; an ordinary
proxy must also advertise preservation across API, signal and service shutdown in an authenticated
read. Older or unverified runtimes are refused before
stop actions. Update/restart that runtime through its supervisor and retry; use ocx disconnect
only when you intend to release the connection-owned integrations.
Repeated stops of an already-stopped client confirm service-manager inactivity and absence at the
known local endpoints before returning the normal not-running result.
While the OpenCodex desktop app is running, ocx stop does not keep the proxy down: the app treats
it as an unexpected exit and starts a proxy again, within seconds for one it started and after about
a minute for one it had only attached to. Use the app’s tray Stop proxy (for a proxy the app
started) or Quit to keep it stopped. For the same reason the dashboard’s Stop button refuses
with desktop_supervised, and changes nothing, for a proxy the app started.
The web dashboard’s Stop button runs the same action (POST /api/stop) on every backend
except Windows Task Scheduler. There the wrapper can respawn the proxy after the task ends,
and only a stop running outside the proxy can verify that restart window before restoring
your client config — so the dashboard refuses with respawnable_service, changes nothing,
and asks you to run ocx stop.
The dashboard also refuses when the proxy is running as the installed launchd or systemd
service. Stopping that manager from inside the proxy would terminate the process before
native Codex is restored, leaving your client config pointed at a proxy that is gone, so the
dashboard returns self_unload_service, changes nothing, and asks you to run ocx stop —
which stops the service from outside and completes the restore.
A proxy exit alone does not confirm that shared Codex/Grok restoration succeeded. If the stop response reports failure, is unreadable, or does not confirm the assigned teardown mode, the CLI keeps restoration with the stopping parent after the existing ownership and respawn checks. It does not enter the forced-stop fallback for a process already observed to have exited. A receipt-backed deferral still leaves final restoration and receipt cleanup with the parent; failure to restore shared client configuration keeps the stop failed and its receipt outstanding.
ocx stop --json runs exactly the same stop path and prints one versioned summary document
(schema: "ocx-stop/1") on stdout, while the human progress lines move to stderr. The summary
carries the outcome class (stopped, not-running, history-incomplete, history-deferred,
or failed), the service and proxy classifications, whether the runtime is down, and a stable
one-line message. Exit codes are identical with and without --json: 0 on success, 1 on failure,
79 when only Codex history cleanup did not complete, 80 when the shared teardown was deferred
and is still owed, and 81 when a dashboard update that will restart the proxy handed Codex routing
and its catalog to the restarted proxy instead of restoring native Codex. With 81 the proxy is
down, and the restarted proxy releases the kept teardown after its first sync; if it never starts,
the next ocx stop restores native Codex.
For a desktop-approved takeover, ocx stop --json also accepts the complete set of
--expect-* values supplied by ocx resolve --json. On Windows, this guarded stop can
stop a Task Scheduler task only when its registered definition, running state, and wrapper
ancestor prove it owns the approved proxy. A WinSW service must prove its installed binary
path and service PID ancestry. After stopping either manager, the CLI signals the approved
proxy separately and verifies that the task is no longer running, no wrapper survives, and
WinSW is stopped. A wscript.exe, cscript.exe, or cmd.exe process with an
unreadable command line in the approved proxy’s supervision chain (or surviving under the
stopped manager) leaves wrapper ownership unknown, so the guarded stop cannot report success.
Missing or unreadable evidence blocks the guarded stop.
ocx restart
Section titled “ocx restart”When this CLI is newer than an attested standalone POSIX proxy, restart can stop the old
installation and launch this one. This guarded update requires an unclaimed physical home,
a detached proxy whose parent is PID 1, positively inactive service supervision, and a known CLI
version. An installed but unloaded launchd or inactive systemd service record is eligible only
when it carries no ownership claim. Both launchd domains must be absent; systemd must report
inactive with MainPID zero. Unreadable or uncertain manager evidence refuses. The command freezes
all service records and the platform definition and checks them through stop, launch and child
publication. Any rewrite, replacement, permission change or record creation/deletion refuses;
retry after inspecting ocx status.
The stop uses the same connection that proved the old proxy’s identity. The command launches
once only after confirmed shutdown, then requires the exact child PID, endpoint, fresh identity
proof and matching version. Missing version, uncertain stop, timeout or an unexpected replacement
reports failure without another stop or start. If the runtime this CLI would launch is still the
small placeholder an in-place npm install leaves before its postinstall, restart refuses before
stopping anything; after a confirmed stop it waits for the runtime within the same deadline and
launches nothing if it does not arrive. Windows, foreground, desktop-supervised, active-service,
connected-client and sibling runtimes do not use this update path; use their owning lifecycle
controls. A newer proxy or incomparable version still refuses an in-place downgrade. Use the newer
installation’s ocx to restart; inspect which -a ocx and ocx status to identify it.
Eligibility refusals name the reason and next action: use the owning installation’s
ocx service restart for a service, the owning terminal for a foreground proxy, or the
owning service/desktop app on Windows. Unknown ancestry or a changed target calls for
ocx status before any restart.
On a connected client runtime (a Remote Link Child or a hub client), ocx restart asks the runtime
to start its own replacement on the same port. If the replacement does not answer within the
restart budget it is stopped, and the old runtime binds the port again and keeps serving; the
command then reports the restart as not completed. A client runtime run by a background service or
the desktop app refuses the request and keeps serving (“Restart not performed; the running client
runtime was kept”); restart it with ocx service restart or from the app. See
Remote Link apply parity.
If package files or the Bun runtime remain incomplete after the installer exited or failed,
run ocx status, stop any running proxy through its owner (ocx stop, or its service or
desktop app), then reinstall with the same package manager and start it again. See
Update failed.
When a proxy is running, ask that exact attested PID and port to restart in place, wait for its
normal drain, and verify a different runtime PID on the same port. Managed routing and service
supervision stay installed throughout; an uncertain request is observed rather than replayed as a
separate stop/start. If no proxy is running, the command falls back to the normal ensure start.
If an accepted restart loses its replacement, the command attempts a bounded recovery start even
when Codex autostart is disabled, and reports failure if no proxy starts.
When the proxy starts its own replacement (no background service supervises it) after the drain
finished normally, a replacement that exits before it answers is started again up to twice. What
the replacement prints goes to ~/.opencodex/restart-handoff.log, which stays near 256 KiB: a
restart empties it once it reaches that size, and the running replacement checks it once a minute.
A proxy the OpenCodex desktop app started does not start its own replacement: it exits, and the app
starts the new proxy on the same port.
If a live listener cannot be attested to a runtime PID (including a pre-update proxy), restart fails
closed without an ensure or stop/start fallback. After confirming ownership, use ocx stop then
ocx start for a standalone proxy. For a service-managed proxy, use ocx stop followed by
ocx service start so supervision is restored.
One invocation re-observes transient discovery races. A failed start is retried only when
the previous attempt never launched a child or the launched child is known to have exited.
If a launch may still be running, restart reports the original failure without starting
a second proxy, even when a health probe finds nothing. When an accepted restart never publishes a replacement, the command re-observes
once before giving up — a proxy that crashed mid-restart reads absent and is started fresh,
while a replacement that landed just past the shortened replacement wait still proves success within the overall observation deadline. Outside the guarded newer-CLI update path, a live target is
never stopped to make room, so a stale-but-listening process can not be replaced by a second
proxy racing it for the port.
Every failed start attempt is followed by a beat and a strong re-observation before the
next attempt: a child that was just launched may still be binding, and post-health steps
may have thrown on an already-serving proxy. A live reading means
no second start is spawned: a clean refusal then attests success, while a throw
propagates as a start failure because post-health work failed on a serving process.
During crash recovery after an accepted live restart, success instead requires a different
runtime PID on the original port: if the original PID reads live again, the restart fails
as a missed replacement rather than reporting success.
Confirmed absence and launch-exit evidence together permit another start attempt. While the previous PID is still live, or a probe is uncertain, the
confirmation window keeps polling for a replacement inside the reserve instead of
failing at once. A second proxy is never spawned next to a live one.
The replacement wait ends early enough that the confirmation window still fits inside the
overall observation deadline.
Port recovery after stop or update respects a failed OCX process check even when the PID was recorded before shutdown. A rejected live holder is left running and prevents TCP-row cleanup. If it stays unverified, the bounded recovery wait can expire with the port still busy. Check the current port holder and retry the restart after the conflict is resolved.
ocx ensure
Section titled “ocx ensure”Idempotently ensure a background proxy is running, then sync its live model catalog. If
codexAutoStart is false, it prints that autostart is disabled and does nothing.
With a validated connected-client configuration, ensure succeeds without starting a local
provider proxy after reconciling the client journal. This does not probe or certify the remote
hub’s availability. Invalid or mismatched client state still fails.
ocx restore [back] · ocx eject [back]
Section titled “ocx restore [back] · ocx eject [back]”Restore native Codex without stopping the proxy — strips the injected config lines and routed
catalog entries so plain codex works natively again. eject is an alias of restore.
Restored catalog output excludes retired native models, including gpt-5.3-codex-spark,
whether stored as bare ids or trusted account-qualified rows. This applies with or without
a catalog backup; the original backup and historical user-selected configuration are preserved.
Restoration reports failure instead of replacing changed configuration files when a saved journal lacks the corresponding injection hashes. The current files and journal remain available for review; see recovery without injection hashes.
Pass back to either spelling to re-point plain codex at an already-running proxy without changing
the proxy lifecycle:
ocx restore backocx eject backocx recover-history --legacy-openai --yes
Section titled “ocx recover-history --legacy-openai --yes”Explicit recovery for older development builds that remapped Codex App history before reversible backup support existed. Close Codex first if its history database is locked.
This is a broad, destructive relabel: every user-message thread currently tagged opencodex is
changed to openai, exec is normalized to cli, and the event marker is set. That includes
legitimate dedicated-provider history. Back up the state and run it only when that full scope is
intended.
ocx recover-history --ocx-compaction <thread-id> --yes
Section titled “ocx recover-history --ocx-compaction <thread-id> --yes”Repair one thread that was compacted through a routed provider before resuming it through native
Codex. The command reads the exact thread selected by UUID, saves a private byte-for-byte backup,
then converts only OpenCodeX-owned ocx1:/ocx2: compaction state into a plain summary that native Codex
can replay. Native encrypted content and other threads are left unchanged. Close the selected
thread before running the command; a concurrent rollout change makes recovery stop without
replacing the file.
ocx uninstall · ocx remove
Section titled “ocx uninstall · ocx remove”Stop the service and proxy, remove the service and Codex shim, restore native Codex, then remove
opencodex local config only if all restore steps succeeded. remove is an alias of uninstall.
Config cleanup requires ownership metadata created by a fresh install; legacy or shared directories
are left in place.
Both commands accept no arguments. Unsupported flags (including --dry-run and --yes)
exit with usage status 2 before preflight or teardown; no changes are made.
For help, run ocx help uninstall or ocx help remove.
Status and health
Section titled “Status and health”ocx status [--json]
Section titled “ocx status [--json]”Status and ocx doctor compare this CLI’s version with the running proxy. If the CLI is newer,
restart the proxy using the intended current installation; for a background service, run
ocx service restart. On macOS, a version skew leaves the service definition byte-identical, so
ocx service repair would reload nothing and keep the old process serving. If the proxy is newer, upgrade the CLI
or resolve PATH to the intended installation. These diagnostics do not repair the service or
change whether requests are allowed.
Identical version strings and the unknown / 0.0.0 placeholders suppress the warning, as does
an absent proxy version. Doctor does not report placeholders as a confirmed match. Different
strings still produce a neutral warning when they cannot be strictly parsed as SemVer or differ
only in build metadata; neither side is called older. Versions are not trimmed and a leading v
is not normalized. JSON exposes the same advice in versionSkew, whose fields remain
cliVersion, proxyVersion, skewed, and warning.
On a Remote Link Child and its Home, --json also reports appliedCodexSettingsDigest,
lastApplyAt and runtimeStartedAt; see Remote Link apply parity.
Print a read-only diagnostic summary: proxy PID, /healthz reachability, dashboard URL, config path,
default provider, Codex autostart setting, service state, shim state, and the redacted effective Codex
home. Only the explicit, high-confidence Windows Orca runtime-home signature adds an actionable App-home
mismatch warning; it never changes CODEX_HOME automatically. It also warns when the Codex routing
opencodex wrote names a local port the running proxy does not serve. ocx sync repairs it
immediately, and a running owner proxy may also re-point it on its own once nothing answers on that
port. A sibling’s status does not report it, because its routing names the proxy it runs beside.
When a live proxy has already passed the identity/liveness check, ocx status prefers that process’s
attested startup-health report for restart safety and service viability. This avoids false negatives
from a shell-local service-manager probe that lacks the running service’s manager environment. The
live report is schema-validated; if it is unavailable or malformed, status falls back to the local
service and shim diagnostics. ocx doctor uses the same live-first rule for its Codex restart safety
section, so the two commands should agree on restart protection. If you are diagnosing a discrepancy,
compare the reported live startup verdict with the local service details rather than treating the shell
probe as more authoritative.
The live read allows the bounded service probe to finish: up to 6.5 seconds on macOS/Linux
and 16.5 seconds on Windows when the diagnostic cache is cold or expired. Cached reads return
promptly. A timeout still falls back to local diagnostics; a healthy /healthz alone does not
establish restart protection.
The clients=pending-restart(...) diagnostic lists Codex CLI clients that predate the routing
injection. On macOS, Electron renderer, utility, and crashpad helpers under Codex.app’s framework
are excluded from that client list, including helpers whose executable paths contain spaces.
Human output also includes an OAuth health block after the OAuth logins summary: OAuth health: ok when every known account is healthy, or OAuth health: warning with one redacted line per
non-healthy account (provider, masked account id, status such as reauthentication required, rate or
quota limited, or refresh conflict) plus an optional Action: hint. Account ids are redacted; tokens
and emails are never printed. The --json contract does not currently include this health block.
ocx statusocx status --jsonAbbreviated example shape:
{ "schemaVersion": 1, "proxy": { "running": false, "pid": null, "health": { "ok": false, "url": "http://127.0.0.1:10100/healthz", "message": "unreachable" } }, "dashboard": { "url": "http://localhost:10100/" }, "paths": { "config": "/Users/example/.opencodex/config.json", "pid": "/Users/example/.opencodex/ocx.pid", "runtime": "/path/to/bun" }, "runtime": { "source": "bundled" }, "codexHome": { "effectiveCodexHome": "C:\\Users\\[USER]\\.codex", "appCodexHome": "C:\\Users\\[USER]\\.codex", "mismatch": false, "warning": null, "action": null }, "codexAutostart": true, "defaultProvider": "openai", "service": { "summary": "not installed (logs: /Users/example/.opencodex/service.log)" }, "codexShim": { "summary": "Codex autostart shim: not installed" }}The real object also includes listen (port, hostname, runtime/config source), config load
diagnostics, and bundled Codex plugin diagnostics. The JSON schema is additive-only: future versions
may add fields, but existing fields should stay stable. It intentionally excludes API keys, OAuth
tokens, authorization headers, request content, emails, and account identities.
ocx health [--json]
Section titled “ocx health [--json]”Identity-check the live proxy. Human output reports PID/port; --json emits {ok, pid, port}. The
command exits 0 only when healthy and 1 otherwise, making it suitable for service probes.
ocx system health [--json]
Section titled “ocx system health [--json]”ocx health --jsonocx system health --jsonRoot health is the liveness probe. System health is a separate management
observation: {status:"ok",service:"opencodex",version,uptime,pid,spendLedger},
with uptime in seconds. system status remains its settings/startup/memory aggregate.
A valid system-health observation exits 0 even if the spend ledger is degraded.
Read ownership held/unheld, initialized, configured, degraded, persistFailures and
corruptRecords; status ok does not certify every subsystem healthy. Failed/malformed
reads remain nonzero with safe stderr, not fabricated healthy data.
For bounded diagnostics, request/injection follow stops on error or detected target change instead of reconnecting automatically. SIGINT exits 130 and SIGTERM 143. Versioned log events reconstruct observed windows, not traffic missed between polls; injection seq cannot exclude restart/eviction gaps.
ocx ready [--json] [--wait [--timeout <seconds>]]
Section titled “ocx ready [--json] [--wait [--timeout <seconds>]]”Check post-sync readiness through the unauthenticated GET /readyz endpoint. It returns 200 when
ready, or 503 with Retry-After: 1 for pending and terminal failed. Its sanitized HTTP identity
is {service, version, uptime, pid, port, status} plus the remote-hub protocol fields
{protocol, minimumClientProtocol, managementUrl, features}. protocol is the hub protocol this proxy
speaks and minimumClientProtocol the oldest client it still accepts, so a client can refuse an
incompatible pairing before sending anything else. managementUrl is the origin a client should
use for the management plane: the configured hub.managementPublicOrigin when runtimeRole is
hub, and otherwise the origin the request itself arrived on. A readiness request with no
HTTP(S) origin is rejected rather than answered with a guess.
features lists the peer features this build serves, so a peer can check a capability without
comparing versions: relay-auth/<n> names the Remote Link relay authentication version,
link-management-relay the read-only Remote Link management relay, link-peer-report/<n> that
a Remote Link Home reports each Child’s version and compatibility in ocx link status, quota-pool/1 the
GET /api/quota-pool management read, and served-by-header the x-opencodex-served-by response
header. A feature this build does not serve is absent. The list holds fixed names only, never
account or configuration data. Old proxies without /readyz fail
closed as unreachable; /healthz is separate liveness, not readiness. The command performs one probe by
default; --wait polls until ready or timeout, but exits immediately when it observes the terminal failed state. The
default timeout is 45 seconds; --timeout <seconds> requires --wait and accepts positive integer seconds from 1–300.
The CLI’s own --json output is deliberately narrower than the HTTP body: it emits
{ready, status, pid, port}, where status is ready, pending, failed, or
unreachable. Exit codes are 0 for ready; 1 for not-ready, pending, failed, timeout, or
unreachable; and 64 for invalid arguments.
On a Remote Link Child, readiness also covers the link. While the link cannot carry a request,
/readyz answers 503 with status: "pending" and link: {state: "unavailable", reason}, so
ocx ready --wait keeps waiting until the link is back; a working link adds
link: {state: "available"} to the 200 answer. /healthz on a Child keeps meaning only that the
process runs. The reasons are listed in Remote Link.
ocx resolve [--json]
Section titled “ocx resolve [--json]”Resolve the runtime facts a shell needs without re-implementing them: the config home, the
effective port, and the identity-checked liveness verdict. --json emits one versioned
document (schema: "ocx-resolve/1") with cliVersion, configHome, port
(effective, configured, and source), and liveness (status, pid, port,
source, plus version, role, and hostname when the live proxy reports them).
When a proxy is live the document also carries versionSkew — the same comparison
ocx status and ocx doctor surface — with cliVersion, proxyVersion, skewed,
a relation of match, cli-newer, proxy-newer, incomparable, or unknown,
and the operator-facing warning. Shells read relation rather than reparsing the
warning; the desktop uses proxy-newer to keep a takeover from downgrading the listener.
Liveness has three answers: live, absent-proven (every recorded and configured endpoint
definitively refused or answered non-opencodex), and unknown — a timed-out probe or a listener
that withholds /healthz exits 1 rather than reading as absent, so only absent-proven may
authorise starting a new runtime. The port is the live listener’s port when a proxy answers,
otherwise the configured port (default 10100). Exit 0 carries a trustworthy verdict; exit 1 means
the CLI could not resolve — including an invalid config.json, which is never repaired to
defaults here — and the caller must refuse to guess; any unknown argument exits 64. Discovery uses
the same ownership-safe probe budget as ocx start, because a false “nothing listening” answer
is how duplicate proxies happen. The verb is read-only and skips the shim auto-restore preflight.
ocx doctor
Section titled “ocx doctor”The default report includes the native-write coordinator state and exact path using immutable read-only SQLite inspection. Zero-byte, empty-unversioned, and rowless states are shown separately from catalog/app-server health, so a successful catalog refresh is not mistaken for successful Codex config injection.
The pending-history check reads a checkpointed WAL-mode state_5.sqlite even when Codex is
closed and its -wal/-shm files are absent. Missing sidecars alone do not indicate a history
integrity failure. Backup manifests and restore targets still undergo integrity validation.
After stopping the OpenCodex proxy/service, explicitly preserve and move a proven non-authoritative coordinator, then retry sync:
ocx doctor --recover-zero-byte-coordinator --yesocx syncThe recovery accepts only a proven zero-byte remnant. It refuses every non-empty, valid, unknown,
changed, unsafe, or busy database and creates a same-directory .zero-byte-backup-* file instead
of deleting anything.
Run read-only environment and connectivity diagnostics: state paths and filesystem type, WSL dual installs, proxy environment/config, ChatGPT reachability, Codex plugin and project-config warnings, and pending history migration. The Codex app-home targeting section also detects the narrow Windows Orca runtime-home mismatch and explains service migration when applicable. Paths shown by this diagnostic redact the OS username. Doctor prints repair hints but does not apply them.
Project-config diagnostics ignore provider examples inside TOML multiline strings, including
developer_instructions. Real provider and profile settings after the closing delimiter are still
checked, even when an escaped quote immediately precedes that delimiter.
The OAuth reliability section reports whether credential storage is writable, whether refresh
single-flight/lock files can be created under OPENCODEX_HOME, non-healthy OAuth or Codex pool
accounts (redacted ids) with a recovery Action:, and a static OK that the Codex forward path does
not fabricate official-client metadata. Doctor never mutates credentials or applies repairs.
Catalog sync
Section titled “Catalog sync”Read saved state separately from synchronization
Section titled “Read saved state separately from synchronization”Model, provider, settings and v2 receipts have different completion contracts.
Local custom-model changes opportunistically sync: no proxy means a successful
save, needsSync:true, sync.status:"not-attempted" and exit 0. An explicit local
provider --sync without a proxy instead returns nonzero after saving.
Settings writes use catalogRefreshPending, while live v2/catalog writes use a
full catalogRefresh disposition. Local v2 mode/keep-native and changed on/off
still attempt their established sync even when no port was discovered. JSON
reports actual changed state and sync evidence; unverified/failed sync is nonzero.
Threads/hints and unchanged on/off do not introduce a new sync attempt.
Inspect the same target with ocx v2 status --json or ocx v2 status --live --json,
and use ocx system settings --json for settings read-back. A saved-but-pending or
unknown outcome does not prove rollback and is not permission to replay a write.
See v2 outcomes and
account policy
for target, scope and spending boundaries. A reset-grant status read can contact
upstream status but never consumes or resumes a grant.
ocx sync [--restart-codex] [--restart-app-server-only]
Section titled “ocx sync [--restart-codex] [--restart-app-server-only]”An explicitly requested restart also runs after a successful committed config.toml change, even if the catalog and cache bytes are unchanged. A configuration-only no-op, refusal, preflight, or rolled-back write does not trigger it. Without a restart flag, sync never automatically restarts Codex.
On a Remote Link Child, ocx sync also applies the Home’s Codex settings and, when the client runtime
started before that apply, restarts it; see Remote Link apply parity.
Fetch the live model list from every configured provider and re-inject the merged catalog into Codex. Run it after adding a provider or to refresh available models.
Before provider discovery or catalog/cache replacement, ocx sync validates that the managed
Codex configuration can be injected. If that validation refuses the config, the command exits
nonzero, prints the concrete reason on stderr, and leaves the existing catalog and cache unchanged.
ocx restore back uses the same no-write preflight before it re-enables routing.
If long-lived Codex app-server processes are still running, ocx sync warns that they may keep
serving the previous in-memory model list even though opencodex-catalog.json / models_cache.json
were updated. Pass --restart-codex to restart matching codex … app-server and
codex-code-mode-host processes and fully quit and relaunch the Codex desktop app, on macOS,
Linux, and Windows, so the model picker re-reads the catalog. Live conversations end. Broad
pkill -f codex matching is intentionally avoided.
--restart-desktop-app is a deprecated alias of --restart-codex. It still works, prints a
deprecation notice, and is not Windows-only.
--restart-app-server-only restores the older, narrower behaviour: SIGTERM only to matching
app-server and code-mode-host processes owned by the current user, with the desktop app left
running. Active turns may still be interrupted. If it is combined with --restart-codex or
--restart-desktop-app, the narrow scope wins, because losing live conversations is unrecoverable
and a stale picker is not.
When the command runs from inside the Codex app, the restart is handed off to a detached helper and this session ends with the app.
Sync only from the expected Home: --expect-link and --expect-hub-key
Section titled “Sync only from the expected Home: --expect-link and --expect-hub-key”ocx sync --expect-link <linkId> [--restart-codex]ocx sync --expect-hub-key <apiKeyId> [--restart-codex]A script that reads ocx connect status --json and then runs ocx sync cannot tell whether the
machine was disconnected or relinked to another Home in between; a plain ocx sync would then
sync the local providers or the other Home’s catalog. Passing the identity you read closes that
gap. --expect-link takes the link.linkId of a Remote Link Child and --expect-hub-key the
apiKeyId of a hub client. The check runs under the same lock that chooses the sync path, and
every later write is bound to the connection that passed it.
When the identity holds, the command is exactly the connected sync above. Otherwise it changes nothing (not even the automatic Codex shim repair other commands run first), never syncs the local providers, exits 5, and prints a human line followed by one JSON line as the last line on stderr:
{"phase":"child_link_mismatch","reason":"other_link"}phase is child_link_mismatch for --expect-link and hub_client_mismatch for
--expect-hub-key.
reason |
Machine state |
|---|---|
disconnected |
Not connected to any Home or hub |
invalid_state |
The client configuration or a connect marker cannot be read |
disconnect_pending |
A disconnect has started and not finished |
pending_operation |
A key rotation, a connect, or another sync is in progress (including one that holds the locks this check needs) |
hub_client |
--expect-link on a hub client |
link_client |
--expect-hub-key on a Remote Link Child |
other_link / other_key |
Connected, but to a different link or with a different key |
A missing or malformed value, a repeated flag, or both flags together exit 2 before any state is
read. A connection that changes after the check, while the catalog is being fetched, still fails
as a plain connected sync does (exit 1, nothing written). Versions without these flags ignore
them, so check ocx --version first.
ocx sync-cache [--restart-codex] [--restart-app-server-only]
Section titled “ocx sync-cache [--restart-codex] [--restart-app-server-only]”Invalidate Codex’s local model picker cache so it is rebuilt from the active opencodex catalog. The
same stale-app-server warning and optional restart flags as ocx sync apply.
If the derived cache already has identical bytes, the command succeeds without rewriting it or restarting Codex. With --json, this is reported as ok: true, wrote: false, skipped: true, and skippedReason: "unchanged"; an invalid catalog or failed cache write still exits nonzero.
Catalog write diagnostics
Section titled “Catalog write diagnostics”Catalog write auditing records bounded diagnostics in opencodex-catalog-audit.jsonl under
CODEX_HOME. An unchanged catalog or cache produces no event; an audit failure does not fail
catalog publication.
On Windows, at most the successful new-file creation event is recorded. The file is created empty and hardened before diagnostic data is written. All later events skip the existing file, both in the same process and after restart. A hardening failure may leave an empty file, which later events also skip. Existing files and their ACLs are not changed for auditing, including when another OpenCodex home owns the catalog. This is a privacy-first fallback, not a complete Windows audit stream; native NTFS behavior for this audit path has not yet been validated.
POSIX auditing continues to append and retain bounded records. The Windows limitation does not
disable catalog protection or owner healing. Audit files in a separate CODEX_HOME remain
cleanup residuals because the config uninstall manifest cannot claim paths outside its own root.
ocx catalog pull <https-url> [--auth-env <NAME>] [--json] [--restart-codex] [--restart-app-server-only]
Section titled “ocx catalog pull <https-url> [--auth-env <NAME>] [--json] [--restart-codex] [--restart-app-server-only]”Install a complete catalog served by another OpenCodex instance’s /v1/catalog endpoint, then
synchronize models_cache.json. Unlike ocx sync, this command does not discover configured
providers or inject Codex configuration. Unlike ocx sync-cache, it replaces the active catalog
before rebuilding the cache. It works even when the local Codex integration desired state is off.
The URL must be HTTPS; loopback HTTP is accepted for local testing. Embedded URL credentials,
queries, fragments, redirects, oversized responses, malformed JSON, duplicate or unsafe slugs, and
unknown input_modalities are refused before any local write.
Loopback HTTP requests are refused before authentication headers are attached or any request is sent when HTTP_PROXY or http_proxy applies without a matching NO_PROXY or no_proxy bypass. ALL_PROXY/all_proxy and settings limited to HTTPS_PROXY/https_proxy do not trigger this HTTP restriction; HTTPS catalog acquisition remains allowed. The refusal message includes neither the proxy address nor the authentication token. Nonempty http_proxy and no_proxy take precedence over HTTP_PROXY and NO_PROXY, respectively. For Bun-compatible bypass rules, use hostnames, matching host:port entries, bracketed IPv6 addresses such as [::1], or *; do not use URLs, paths, or *. prefixes.
Authentication is optional and is read only by environment-variable reference:
export OPENCODEX_CATALOG_AUTH_TOKEN='...'ocx catalog pull https://proxy.example.com/v1/catalog \ --auth-env OPENCODEX_CATALOG_AUTH_TOKENThe value is sent as a Bearer token but is never accepted as an argv value. Redirects are refused,
so authorization cannot cross origins. Catalog and cache writes use the shared Codex catalog lock
and atomic writer. A failed fetch, validation, lock acquisition, catalog write, or cache rebuild
preserves the last-known-good files. Identical catalog bytes are a no-op that preserves mtimes and
never touches processes. --restart-codex, --restart-app-server-only, and the deprecated
--restart-desktop-app alias mean the same thing here as they do on ocx sync and
ocx sync-cache, and they apply only after a real write.
The URL must name /v1/catalog at the host root. A reverse proxy that serves the endpoint under a
path prefix is not supported by this command.
The command downloads the full catalog and compares bytes locally instead of issuing an ETag /
If-None-Match conditional request. Identical bytes are treated as a complete no-op, so a
Codex home whose catalog is correct but whose models_cache.json is missing or stale is not
repaired by this command; use ocx sync-cache for that.
--json emits one stable envelope on stdout. schemaVersion, ok, status, catalogWritten,
cacheSynced, and codexRestarted are always present. codexRestarted still means app-servers
only. desktopAppRestarted is present only when a desktop restart was requested, and is true
only when the relaunch actually started; a handoff is not a success. status is updated,
unchanged, or failed. A successful pull adds modelCount; a failure adds code, which is the
field a script branches on:
code |
Meaning | Exit |
|---|---|---|
usage |
The arguments were not a valid catalog pull invocation |
2 |
auth_env_missing |
--auth-env named a variable that is not set |
1 |
url_invalid, insecure_http_refused |
The URL was refused before any request | 1 |
request_failed, redirect_refused, http_error |
The request did not produce a usable response | 1 |
body_too_large, body_invalid, catalog_invalid |
The response was refused before any local write | 1 |
write_failed, lock_database, unsafe_path |
The coordinated write did not complete; files are unchanged | 1 |
lock_busy |
Another writer holds the Codex catalog lock | 3 |
restart_incomplete |
The catalog and cache landed, but a Codex app-server survived --restart-codex or --restart-app-server-only |
1 |
restart_incomplete is the one failure that reports real writes: catalogWritten and
cacheSynced stay true and ok is false, because a surviving app-server still serves the
previous catalog from memory.
Background service
Section titled “Background service”A managed service may start a newer installation that previously served the same
OpenCodex home, even when its service definition still points to an older install.
If that newer child exits before binding, with any exit code, the original service starts instead
and applies its own checks before serving.
Foreground starts and client-launched proxies keep their selected executable.
On Windows, this applies to Task Scheduler and newly installed or repaired native WinSW
services. A native WinSW definition created before this behavior needs ocx service repair
to refresh its XML before it can hand off to a newer install.
ocx service [install|repair|restart|start|stop|status|uninstall|remove]
Section titled “ocx service [install|repair|restart|start|stop|status|uninstall|remove]”On Windows Task Scheduler, the service wrapper restarts the proxy after five seconds
even when an external tool terminates it with exit code 0. If another opencodex proxy
already owns the port, the wrapper exits deliberately. Use ocx stop or
ocx service stop to stop the service and its restart loop. After upgrading an
existing installation, run ocx service repair to refresh the generated wrapper.
Run opencodex as a login-managed background service (macOS launchd, Linux systemd user unit,
Windows Task Scheduler) that auto-starts on login and auto-restarts on crash. Service runs set
OCX_SERVICE=1 so a restart does not churn the Codex config.
Windows Task Scheduler installs use normal process priority (Priority=4). The older background
priority (7, also the scheduler default when omitted) can delay the proxy’s health responses under
CPU contention, making the tray report Offline even while the process is alive. After upgrading,
run ocx service repair to migrate that registered priority and restart the service. This migration
may request UAC approval; a priority already set to normal or high does not itself trigger replacement.
The Windows wrapper supports locale dates containing parentheses, including Korean and Japanese
date formats. After upgrading, run ocx service repair to replace an older generated wrapper
that exits before launching Bun on those locales.
The Windows wrapper verifies its baked Bun runtime and CLI entry before every start attempt. If an
interrupted package update removed either file, it logs one installation is incomplete message and
stops instead of retrying the same missing executable every five seconds. Reinstall opencodex, then
run ocx service repair to refresh the task with the restored package paths.
Service definitions omit shell-local version-manager multishell directories from their saved
PATH. This includes the native Windows service installed with ocx service install --native;
run ocx service repair to regenerate an older WinSW definition with a durable PATH.
On Linux, the systemd unit invokes the first regular, executable ocx file found on PATH at
install time rather than the Bun and CLI paths inside the installed package tree. Version managers
such as mise and asdf install into a versioned directory and delete the old one on upgrade;
their stable shim keeps the unit resolving. Source checkouts without an ocx launcher keep the
direct Bun + CLI form. A trusted OPENCODEX_BUN_PATH selected before Bun starts is preserved
through the shim; package-local bundled Bun paths are rediscovered after upgrades.
On macOS, launchd instead uses the package-local Bun and CLI paths selected during install or
repair. This prevents a mutable PATH shim from receiving the service API token and configured proxy
environment on a later restart. After upgrading a version-manager installation, run
ocx service repair to refresh those paths before restarting the service.
Launchd restarts the proxy after a crash or failed restart handoff, but leaves it stopped when
the service deliberately exits cleanly because the desktop app owns the runtime. Run
ocx service repair once to apply this behavior to a service installed by an older version.
Definitions installed before this change still carry the old versioned paths and cannot migrate
themselves — once the old executable is deleted, no opencodex code runs to fix it. Run
ocx service repair once after upgrading. Linux service starts then follow the launcher; macOS
repair writes the new package paths into the launchd definition.
An already-running proxy is not replaced by an external upgrade: when the installed CLI is newer
than the running proxy, run ocx service restart so the new build serves. On macOS, repair is not
enough there: the definition did not change, and a repair that changes nothing reloads nothing.
If the proxy is newer instead, check the CLI installation and PATH as described under
ocx status.
| Subcommand | Action |
|---|---|
| none | Install and start when absent; otherwise repair the existing service. A healthy Windows scheduler definition is reused; a stale definition may be re-registered and require elevation. |
install |
Create and start the service. Registers it, which on Windows needs elevation. |
repair |
Refresh an installed service in place. On macOS, the manager is reloaded only when something changed, so a healthy, unchanged job keeps running and the repair is not an outage. On Linux and Windows, the service is restarted; a healthy Windows scheduler definition is reused, while a stale definition may be re-registered and require elevation. |
restart |
The same refresh and a guaranteed restart on every platform. On macOS an unchanged, already-loaded job is kickstarted in place. Not an alias of repair. |
start |
Start an installed service. |
stop |
Stop the service and restore native Codex. |
status |
Report service and proxy diagnostics plus log paths. --json prints the passive document described below. |
uninstall |
Remove the service and restore native Codex. |
remove |
Alias of uninstall. |
On Windows, a bare ocx service runs the install path only after both Task Scheduler and WinSW are
proven absent. If either status query is inconclusive, it refuses to register anything and asks you
to run ocx service status; use explicit ocx service install only after confirming absence.
ocx service status --json prints one passive document for scripts and peers such as GOAT’s device inventory. It runs the same reads as the text report and never installs, starts, stops or repairs anything; --json on any other service verb is rejected. Fields: schemaVersion (1), platform, supported, backend (launchd, systemd, schtasks, winsw, or none when nothing is registered), label (the job, unit, task or WinSW service name), installed, enabled, running (the service manager’s verdict), viable, stale, conflict, serving (probed, ok, port; probed only when a registration exists), summary, logPath, ownershipLease and desktopLogin. An unsupported platform or a Docker container still gets a document, with supported: false.
Runtime ownership
Section titled “Runtime ownership”The OpenCodex desktop app can take the background proxy over from a CLI installation. When it does, it records the handover in the shared service install state, and that record is what makes the takeover survive a restart. Your service registration is kept, never deleted — the record supersedes it rather than replacing it.
A state file with no ownership record means the CLI installation owns the runtime, which is what every installation made before this feature is in. Nothing changes for you until an app takes over.
Supervised service children also check the recorded owner before startup and once more while holding the startup ownership lease, before choosing a port or publishing a PID. If the desktop app claims the runtime during startup, the service child stands down even if the desktop proxy has not begun listening yet. An unreadable ownership record has the same stand-down behavior.
Home paths inside a state record are compared with the current home by the physical directory they resolve to, not just their spelling. A junction or symlink recorded under an older install still names the same home and keeps working after the move; an alias that no longer resolves is only treated as a different home when its recorded spelling also differs from the current one, so a stale mount still produces the foreign-owner refusal instead of silently claiming the runtime.
While something other than this CLI owns the runtime, the subcommands that would activate your registration refuse instead:
| Subcommand | Behaviour under a foreign owner |
|---|---|
repair, restart |
Refuse before changing anything. The registration is not re-enabled, rewritten or restarted. |
start |
Refuses for the same reason, so an automatic tray start cannot put a second proxy beside the app’s. |
stop, uninstall |
Unchanged. They deactivate, so they are never gated. |
install |
Takes the runtime back. It clears the ownership record after the registration succeeds, and reports whose it was. |
ocx update behaves the same way: it neither stops the running proxy nor refreshes the service
while the app owns the runtime, because the running server is the app’s own bundled binary and the
refresh would re-enable the launcher the takeover superseded. The app updates its own runtime.
The refusal names the owning installation and the consent generation, for example:
Background service repair stopped: the desktop app owns the runtime (install <id>, consent generation 2).The service registration was left exactly as it is — not re-enabled, not rewritten and not restarted.Quit the desktop app and run 'ocx service install' to hand the runtime back to this CLI.A record that cannot be read or does not parse produces the same refusal with a different first line, because an unreadable claim is not the same as no claim — treating it as “nobody owns this” is how a permissions error would silently reactivate your service.
Recovery in every case is ocx service install. It is deliberately the one verb that is never
gated, so removing the app without handing the runtime back, or a corrupted state file, still leaves
you a way to take the service back:
ocx service installocx serviceocx service installocx service repairocx service restartocx service statusocx service uninstallinstall, start, and repair confirm that a proxy actually answers on the port
baked into the installed service before reporting success — on all three platforms.
They wait up to 20 seconds and then print the serving port:
✅ opencodex service installed and serving on port 10100.If nothing answers, they warn and exit non-zero:
⚠️ Service installed, but no proxy answered on port 10100 within 20s. The manager registered the job; that is not the same as serving. Log: ~/.opencodex/service.log Meanwhile: ocx start (serves in the foreground)A non-zero exit here means registered but not serving — not not installed. The
service manager accepted the job; the proxy behind it never bound the port. Read the
log named in the message, and use ocx start to serve in the foreground meanwhile.
ocx service status reports the same three states rather than raw manager output:
✅ installed and loaded (launchd; logs: …) Serving on port 10100.⚠️ installed and loaded (launchd; logs: …) Registered, but no proxy is answering on port 10100. launchd is running an OLDER plist than the one on disk. Fix: launchctl bootout gui/$(id -u)/com.opencodex.proxy && ocx service repair Log: ~/.opencodex/service.log Repair: ocx service repair Meanwhile: ocx start (serves in the foreground)It no longer prints the raw launchctl list / systemctl status line, which
reported a registered job identically whether it was serving, bound to nothing, or
running a previous definition. The Diagnostics: line still carries the log path and
any stale-baked-path finding.
On Windows the scheduler backend keeps its own richer status output, which already reported Task Scheduler registration separately from proxy reachability.
Service logs
Section titled “Service logs”The managed proxy writes stdout and stderr to ~/.opencodex/service.log
(or $OPENCODEX_HOME/service.log with a custom home). When OCX_SERVICE is set,
ocx start prefixes every console log line, including multiline diagnostics,
with a UTC ISO-8601 timestamp such as 2026-10-14T03:04:05.678Z.
Foreground output and other CLI commands keep their existing format.
The service checks the log size at startup and once per minute. Above 20 MiB it
copies the newest 20 MiB into service.log.1, moves the previous archive to
service.log.2, and truncates the active file in place so the service manager’s
open append handles remain usable. The active file can exceed the threshold
between checks; each archive is limited to 20 MiB. Rotation failures do not stop
the service. Copy-truncate can lose lines written during the copy, and an archive
may begin partway through a line. Existing Windows wrapper messages keep their
wrapper format.
On macOS this also covers a subtler failure: launchctl load reports failure on
stderr while exiting 0, so a load that did not take used to leave launchd running a
previous version of the service definition while the command printed a checkmark.
install now fails loudly in that case and names the launchctl bootout command that
clears the stale job.
On Windows, ocx service status reports Task Scheduler registration separately from
identity-verified OpenCodex proxy reachability. It does not print the localized schtasks table,
so the summary remains readable across Windows code pages. Transactional-backup recovery
logs a fixed success message rather than the backup directory name; backup names containing
shell metacharacters do not become commands in that log message.
On Windows, uninstalling an elevated-registered opencodex-proxy task can require an
elevated terminal. If deletion is denied, or exits with code 1 while the task is still
present, OpenCodex reports the elevation requirement and preserves the service assets.
Open PowerShell or Terminal with Run as administrator and run:
schtasks /delete /tn opencodex-proxy /fThen rerun ocx service uninstall from the original account to finish cleanup, and check
ocx service status. Access-denied start/stop commands name their corresponding elevated
schtasks command. Scheduler failures use stable diagnostics and exit codes rather than
echoing localized child stderr, which may use a different Windows code page. An absent or
unreadable task after a failed delete keeps exit code 1 alone from confirming an access
denial; recognized access-denied text from the delete is still classified as one.
On Windows, creating the Task Scheduler entry requires elevation. Recognized localized
access-denied text keeps the existing guidance path. If that text is unreadable, the fallback
requires the owned command shape /create /tn opencodex-proxy /xml <non-empty-path> /f, status 1,
and a confirmed non-elevated token; the dashboard’s Startup Safety action can then request UAC
automatically. If that fallback cannot determine the token state, it retains the original scheduler
error. Foreign tasks and operations can never emit the automatic-elevation marker. Approve the
dashboard UAC prompt or rerun ocx service install in an elevated PowerShell window.
For a fresh install where the OpenCodex scheduler task is confirmed absent, UAC approval now happens before the installer stops any existing proxy. Its unique registration XML is staged in an ACL-hardened private directory outside the OpenCodex config root, and the task is registered without being run. Only after registration succeeds does OpenCodex remove that XML, require ownership metadata for a genuinely new config root, stop the old listener, remove and boundedly re-verify any native WinSW registration, publish the service assets, and start the scheduled task. Cancelling or denying UAC, or failing to claim a new root safely, therefore leaves the working proxy and its Codex routing in place. Existing or conflicting scheduler registrations continue to fail closed rather than being deleted as an unsafe best-effort rollback.
If startup reports another process owns the runtime mutation lease or ocx service status shows
Runtime mutation lease busy, the lease is blocking startup or service changes even if the
proxy is not running. The message includes the lock path, recorded PID, current liveness,
executable name when available, and lease age. The process identity is unverified: the PID
may have been reused, so liveness and executable name describe whichever process occupies
that PID now. Wait for the operation to finish and retry; do not delete the lock or stop a
process based only on this PID. A later mutation attempt can reclaim a stale lease once its
age exceeds 30 seconds and the recorded PID is no longer alive; status only inspects it.
On Windows, this shared Node/Bun diagnostic does not launch an executable to look up the image name; the recorded PID, liveness, lease age and recovery guidance remain available.
ocx codex-shim <install|status|uninstall|remove>
Section titled “ocx codex-shim <install|status|uninstall|remove>”On macOS and Linux, install a private autostart wrapper at <OPENCODEX_HOME>/bin/codex and a
sourceable <OPENCODEX_HOME>/codex-shell-env.sh, using the resolved OpenCodex home. The native
launcher stays where brew, npm, or fnm installed it; package-manager upgrades and version rollbacks
continue to work without rewrapping that launcher. On Windows, script-based launchers are still
wrapped in place. Real codex.exe targets are left untouched.
If installation is refused or the wrapper cannot run, the command exits nonzero and the dashboard
reports the failure reason. A runnable Unix wrapper counts as a successful install even before
activation in the current shell; an already-installed healthy shim also counts as success.
For Windows installations that expose only codex.exe, use ocx service install for autostart.
On macOS/APFS, a reboot can change filesystem device numbers. An unchanged private wrapper and shell environment remain owned after that change, so status, repair, and uninstall continue to work. OpenCodex still checks their recorded inodes, paths, owner, permissions, and content/launcher binding; replaced or unvalidated files are preserved.
After brew/fnm and other PATH setup, run the activation command printed by install. With the default home, it is:
. "$HOME/.opencodex/codex-shell-env.sh"For a custom home, use the printed, quoted path. Sourcing is idempotent: it removes duplicate private
bin entries and puts that directory first on PATH. To activate future shells, add that source line
after your package-manager PATH setup yourself; OpenCodex never edits shell startup files. Install
cannot change its parent shell. ocx status, ocx codex-shim status, ocx doctor, and ocx connect
report a runnable but unselected wrapper as not active, with the activation command. Shell
aliases/functions and separately configured desktop or service launchers still need their own setup.
For fnm-managed Codex, installation resolves the temporary multishell directory to the durable
Node installation while preserving the launcher filename. If that directory cannot be resolved,
installation refuses instead of wrapping a temporary path. When Codex is selected, ocx connect
also reports shim readiness. This readiness check skips special-file PATH entries and, on macOS
and Linux, files without execute permission, including symlink targets. It preserves the order of
regular executable launchers, including npm and fnm symlinks. Windows keeps its PATHEXT lookup.
If a different PATH wrapper hides a runnable shim, fix PATH order;
reinstalling the same shim does not change which command your shell finds first. If the tracked shim
is runnable but no codex command is found, connect reports it as inactive and prints the activation
command. A failed PATH inspection reports activation as unverified instead of claiming
that no command exists. These warnings do not change the connect command’s exit status.
Before an install or repair is committed, OpenCodex runs the saved launcher with --version while
service startup is bypassed. It refuses the change and rolls back when the launcher resolves
codex back to the shim, exits nonzero, exceeds five seconds, leaves descendants running, or
cannot be validated and cleaned up safely. Therefore codex-shim install is not unconditional. If
it is refused, reinstall Codex so the PATH entry is a concrete executable or launcher and retry;
use ocx service install instead when a dynamic command-manager launcher cannot meet these checks.
Cleanup refusals include a bounded diagnostic suffix identifying the probe phase, a recognized
native error code or signal, and the exit status when known. It does not include launcher paths
or raw child output, and does not relax the validation or rollback checks.
An older Unix shim installed in place is migrated only by an explicit ocx codex-shim install.
Migration restores the recorded native launcher without replacing a newer launcher already present,
then installs the private wrapper. If private installation fails after native restoration, the native
launcher stays restored and the operation can be retried. Missing or unusable native launchers require
package-manager repair; OpenCodex does not guess another installation or rewrap the manager’s path.
Migration also refuses when the recorded codex-shim.json is not a regular file owned by you or is
group- or world-writable. Older releases wrote that file with the process umask, so a umask of 002
left it at mode 0664. The refusal names the file and the chmod 600 that clears only the
group- or world-writable refusal. Symlink and foreign-owner refusals remain; OpenCodex does not
change the permissions itself.
Launcher installation alone does not prove that Codex requests will use OpenCodex. After a runnable
install, the command checks the current Codex routing and reports a warning instead of a green result
when routing is external, user-owned, or unverifiable. It also warns when outbound proxy variables
exist only in the current process while config.proxy is unset or unresolved, because Codex
launchers and background services may not inherit that environment. These checks are read-only and
never print proxy values; resolve the reported handoff and run ocx doctor before relying on
autostart.
On Unix, automatic repair refreshes only the private wrapper and never rewrites package-manager
launchers or migrates an older in-place shim. On Windows, if a completed external Codex update
overwrites an installed shim, the next ordinary ocx command backs up the stable new launcher
and restores the shim before dispatch. The zero-effect
ocx system codex-cli-update check inspection command and malformed invocations in its reserved
ocx system codex-cli-update namespace never perform that repair. ocx status, ocx doctor, and
ocx codex-shim status also skip shim auto-repair and report the observed state.
A launcher that is still changing is left untouched and retried later. Repair failures warn without failing the requested
command; manual fallback: ocx codex-shim install. Set codexShimAutoRestore to false, or set
OPENCODEX_CODEX_SHIM_AUTO_RESTORE=0 for a process-level opt-out.
The Unix wrapper retains its recorded native launcher path. Package-manager changes behind that durable entry are followed automatically; removing that entry requires package-manager repair. Dynamic command-manager launchers still have to pass the validation above. Windows in-place repair needs the saved original beside the shim; if a version manager destroys the whole tree and backup, OpenCodex reports the condition and stops instead of inventing a replacement original.
If your version-manager launcher cannot pass validation, route through Codex configuration instead of the
launcher: ocx start writes openai_base_url, and ocx service install provides autostart. Run
ocx status to confirm — it reports the active routing, and warns when a running proxy is not the
one Codex is pointed at.
| Subcommand | Action |
|---|---|
install |
Install the shim (or repair if stale). |
uninstall |
Remove private Unix artifacts, leaving native Codex untouched; on Windows, restore the original launcher. |
remove |
Alias of uninstall. |
status |
Report shim state and whether the private wrapper is active on PATH. |
ocx codex-shim installocx codex-shim statusocx codex-shim uninstallAfter Unix uninstall, remove the source line from your shell startup file and restart the shell or remove the private bin entry from its PATH. Uninstall removes the owned wrapper, shell environment file, and state; it leaves the package-manager launcher untouched. An older in-place Unix shim is released using its recorded restoration data.
Token injection into Codex
Section titled “Token injection into Codex”On a non-loopback bind the injected provider carries env_key = "OPENCODEX_API_AUTH_TOKEN". That
line tells Codex which variable to read; it does not create it. Codex refuses to start a request
when the variable is missing (Missing environment variable: OPENCODEX_API_AUTH_TOKEN), and the
proxy is never reached. The value lives in $OPENCODEX_HOME/service-api-token; the launching process
must supply it in Codex’s environment.
Use the maintained shim installed by ocx codex-shim install. When the launching context resolves
this shim, it reads the token file created by OpenCodex and supplies the variable to Codex.
Desktop, cron, and service launches must use a PATH or launcher path that selects the shim;
installation does not configure those environments automatically. Codex’s own child processes
may still inherit the token.
Do not export this bearer token from a shell startup file or copy it into config.toml. The
service-api-token file contains the raw token, not NAME=value assignments, so it cannot be used
directly as a systemd EnvironmentFile=.
An EnvironmentFile= or OCX_API_TOKEN_FILE on opencodex-proxy.service configures the proxy process
only and never flows into an independently launched codex exec.
Unix package-manager upgrades leave the private wrapper intact. A runnable wrapper that is not
active does not inject the token: source the printed activation file in the launching shell.
On Windows or with an older in-place shim, a Codex upgrade that replaces the launcher removes the shim;
on Windows the next ordinary ocx command restores
it (see above), but a codex exec that runs before that fails. ocx doctor reports this exact
state under “Codex env_key launch readiness” (env_key configured, variable unset, shim missing or
unhealthy, token file present) with repair or activation guidance, and never prints the token. Reading the token
file is not part of the injected env_key contract; the launching process must supply that variable.
ocx tray <install|start|stop|status|uninstall|remove> [--json] [--no-start]
Section titled “ocx tray <install|start|stop|status|uninstall|remove> [--json] [--no-start]”Install and control the Windows status tray icon. It starts at Windows login and provides one-click
proxy controls. start and stop control the icon only; use its menu to control the proxy.
--no-start applies to install and installs the tray without launching it immediately.
Deprecated: the OpenCodex desktop app provides the tray on Windows, macOS, and Linux; ocx tray
remains for installs without the desktop app.
When a newer package version is known, the tray adds a blue dot to its online, warning, or offline icon and shows Update available. The tray checks its local cached badge about once a minute; stale or unavailable results remove the dot. The menu item opens the dashboard, where you can start the package update. It does not install automatically.
Dashboard
Section titled “Dashboard”ocx gui
Section titled “ocx gui”Open the web dashboard at http://127.0.0.1:<port> — or at
http://127.0.0.1:<management port> when hub management ingress is enabled — auto-starting the proxy
if it is not running.
Updating
Section titled “Updating”ocx update updates OpenCodex itself; it does not update the Codex CLI. Use the
system inspection commands to inspect the configured Codex CLI candidate
with bounded, read-only provenance inspection. ocx system codex-cli-update check does not query a
package registry or install an update.
ocx update [--tag latest|preview]
Section titled “ocx update [--tag latest|preview]”Compiled standalone installs, including the Windows MSI Desktop bundle, are updated through Check for Updates… in OpenCodex Desktop, or by reinstalling the standalone binary from GitHub Releases. ocx system update check reports installer: "standalone", canUpdate: false, and reason: "standalone_install" with that guidance. ocx update exits unsuccessfully with the same hint; it does not run a package-manager update for the compiled executable. A genuine source checkout still reports installer: "source" and reason: "source_checkout".
When OpenCodex is installed through mise, this command exits unsuccessfully before stopping the proxy or changing package files and shows mise upgrade <tool>, using the verified local mise alias. Update checks remain available and report the installation as externally managed. An unreadable or inconsistent mise ownership record fails closed without guessing a tool name, and --tag preview never changes mise’s configured selection.
On Linux, a background service whose recorded launcher is mise’s package launcher (<tool>/latest/node_modules/.bin/ocx, not a mise shim) follows mise upgrade by itself: within about ten seconds of the new version settling, it drains active requests and restarts onto it, and it recovers the same way if mise later prunes the version it was running. On macOS, for a service installed through a mise shim, and for a foreground proxy, restart it yourself after upgrading (on macOS, ocx service repair first).
Self-update opencodex from npm. Stable installs use @latest; preview installs stay on @preview
unless you pass --tag latest|preview. It detects a source checkout and tells you to
git pull && bun install instead, and is a no-op if you are already on the newest version for that
tag. Before stopping anything, npm installations run a bounded Unix cache ownership and access
check. Nested symlinks are checked with lstat but not followed; Windows explicitly skips this
Unix-only check. A failure aborts while the tray and proxy are still running. A running proxy is
then stopped before files are replaced; an installed service is rebuilt and started automatically,
while a foreground installation prints ocx start as the next step. Dashboard update records
redact profile/cache paths and UID/GID values before they are persisted.
When a stopped listener’s literal IP address drops the liveness dial instead of refusing it
(for example on a tailnet), the updater briefly tries binding that same address and port.
A successful bind confirms absence at that instant; a failed bind or an inconclusive hostname
probe still prevents the update.
On Windows, Scoop’s default nodejs and nodejs-lts npm installations can be used
from the user home directory when their current junction stays inside the Node app
directory; the default persistent bin directory is also supported. Running inside
that installation (including its resolved version directory or persistent bin),
project-local npm, NO_JUNCTION version-directory entries, and custom Scoop roots
under the home remain excluded.
If the install step fails, the previous version stays installed and its service is restarted; the terminal output names the next step, and Update Failed on Windows covers finishing the update and the folders a failed attempt can leave behind.
ocx updateocx update --tag previewNew versions become available when the Release workflow publishes them to npm.
Save cleanup thresholds without starting a cleanup
Section titled “Save cleanup thresholds without starting a cleanup”ocx storage policy show --jsonocx storage policy set --archived-bytes-over 1073741824 --reduce-to-bytes 536870912 --jsonocx storage policy show --jsonThe setter writes nested trigger.archivedBytesOver and one target. Byte values
are safe nonnegative integers. Alternatively choose --remove-oldest-percent 10
or legacy --percent 10; the two percentage flags and reduce-to are mutually
exclusive. CLI percentages are integers with server-supported range 1–100.
Omitted --enabled leaves the stored value unchanged; explicit values are true
and false. Set does not run cleanup, but an already-enabled schedule remains
in force. Do not add enable or an immediate policy run unless intended.
The new-field receipt is {ok:true,policy,job:{status}}, with observed trigger,
target, mode, schedule and optional last/next-run facts. job.status describes an
existing idle/running state, not a cleanup started by this command. Read back on
an unknown outcome rather than replaying the write.
Remote Link apply parity
Section titled “Remote Link apply parity”A Remote Link Child applies its Home’s OpenCodex-owned Codex settings when it is linked
(ocx link add on the Home) and on every ocx sync. Three facts let a peer check that the
Child really runs under the settings it applied, without reading either machine’s configuration.
ocx status --json, on any device, carries:
| Field | Meaning |
|---|---|
appliedCodexSettingsDigest |
Lowercase hex SHA-256 of the applied settings (below). On a Child, the values its last recorded apply wrote. On a Home or standalone install, the settings document it serves its Children right now, computed from its current configuration. null on a hub client, on a Child with no recorded apply, and on a Child whose apply of changed settings is in progress or failed (the previous digest is withdrawn first, so it is never reported for values Codex no longer has). |
lastApplyAt |
Child only: epoch milliseconds of the last successful apply that changed the applied settings. Re-applying the same values is not a new apply and keeps it, so an unchanged ocx sync never forces a restart. null elsewhere. |
runtimeStartedAt |
Child only: epoch milliseconds the running client runtime started. null when no client runtime answers. |
The digest is computed over this compact UTF-8 JSON, with the keys in exactly this order and no whitespace, from the values actually applied:
{"version":1,"settings":{"fastMode":true,"multiAgentMode":"v1","webSearchSidecarEnabled":false,"subagentDefaults":null}}subagentDefaults is null when unset, otherwise {"model":…,"reasoningEffort":…} in that order.
The example above hashes to 57c751eff89844f93db71a6ee4acdac03afbc29fec52734ade80a40193e826cd. The digest
never contains a setting value, and the Home’s name and version are not part of it, so a Home and
a Child that applied the same values report the same digest.
On the Home, every row of ocx link status --json carries what that Child last reported:
lastApplyAt, runtimeStartedAt, appliedCodexSettingsDigest (same meanings as above) and
reportedAt (epoch milliseconds, the Home’s clock). A linked Child reports shortly after its runtime
starts and then once a minute; the Home keeps reports in memory only, so after a Home restart the
fields read null until the next report. A Child whose runtime started before lastApplyAt is
running an older runtime than its last apply.
Remote Link Child attestations
Section titled “Remote Link Child attestations”A peer that sends work through a Remote Link Child can check two more facts in the Child’s
ocx status --json. Both fields are omitted, never null, when they cannot be proven.
| Field | Meaning |
|---|---|
localCredentials |
{count, generation}. count is the number of provider credentials this installation stores on this machine: OAuth accounts, provider API-key slots (including disabled entries and keychain or environment references), Codex account-pool records, the native Codex auth.json and inactive native Codex profiles. Link keys, management tokens and incoming API keys do not count, and neither do the accounts a Home serves. generation is opaque and changes whenever a stored credential is added, replaced or removed, even when the count ends where it started. Reported only while an OpenCodex runtime on this machine is running and every store reads completely. A native Codex configuration whose cli_auth_credentials_store is not file omits it. |
link.connectionGeneration |
On a Child: opaque, and changes whenever the tunnel to its Home connects (including a reconnect that keeps the same link) and whenever either runtime restarts. Reported only while the Child’s runtime is ready. For a link started from Home, the Child learns it from the Home’s signed answer, so a Home on an older release leaves it out. |
A Child that reports localCredentials.count 0, with localCredentials.generation and
link.connectionGeneration unchanged across a request, had no local provider credential during it
and observed no tunnel reconnect.
For a link the Home added, each row also carries tunnel: attempt (ssh attempts since the link
was last connected), nextRetryAt (ISO time of the next scheduled retry, null while one runs or
none is due), lastTransition ({from, to, at, cause}) and lastFailure ({cause, at}). A cause
is one of auth, hostkey, forward, remote_forward_conflict, timeout, network,
unknown, spawn_failed and listener_unavailable. The Home keeps it in memory, so it starts
empty after a Home restart. GET /api/link/status includes the field only when asked with
?diagnostics=1, because an older ocx link status rejects fields it does not know.
After ocx sync applies the Home’s settings on a Child, it restarts the Child’s client runtime when
that runtime started before lastApplyAt, using the same restart as ocx restart. A runtime that
already started after the apply is left alone, nothing is started when no client runtime is
running, and a failed restart exits nonzero while the previous runtime keeps serving; run
ocx sync or ocx restart again to retry. Two applies on one Child never interleave: a second
ocx sync while one is applying fails fast and changes nothing.
Forced link revoke and remote recovery
Section titled “Forced link revoke and remote recovery”Only when skipping remote cleanup is explicitly intended, use the actual selected
link ID (lnk_0123456789abcdef below is fictional):
ocx link revoke --link-id lnk_0123456789abcdef --force --yes --json--force requires --yes; --yes alone is invalid. Ordinary revoke is unchanged.
A forced success returns {linkId,remoteCleanup:"skipped",recovery}. Idempotent
link-not-found instead reports remoteCleanup:"unverified". Both can exit 0;
these facts derive from CLI intent, not a server cleanup report. Neither means
remote disconnect was attempted or confirmed. Follow the recovery instruction,
ocx disconnect on the remote client, when that action is authorized.
Observe Remote Workspace from the Hub
Section titled “Observe Remote Workspace from the Hub”ocx remote-workspace hub status --jsonocx remote-workspace hub runtimes --jsonocx remote-workspace hub sessions --jsonThese fixed management reads are separate from executor-local
ocx remote-workspace status. Runtime availability is a keyed object; an available
Hub with no devices/sessions is empty success. Disabled Hub observation can arrive
as HTTP 200 with available:false yet exits 1; it is not empty success. Inner
HTTP 409 exits 5. Reads never enroll devices, create sessions or submit prompts.
See Hub observation and recovery.
Remote Hub client lifecycle
Section titled “Remote Hub client lifecycle”Use ocx connect <url> --pairing-code-stdin, ocx connect status, ocx sync, and ocx connect rotate --pairing-code-stdin. The initial catalog download fails after five seconds without incoming bytes, but active transfers may run longer; use --catalog-timeout <seconds> (1–120) to override that inactivity window. ocx disconnect restores local state offline and does not revoke the hub key. While connected only, ocx connect revoke --admin-token-stdin revokes the persisted apiKeyId; after disconnect use the hub’s Connect → API Keys page. Secrets are stdin-only and never belong in argv.
Setup port validation
Section titled “Setup port validation”ocx init accepts TCP ports 1–65535 as decimal whole numbers. Press Enter to use 10100. Invalid input such as 0, 10100oops or 1.5 is never silently replaced or truncated; setup reports it and asks for the port again.
Login registration diagnostics
Section titled “Login registration diagnostics”On Linux, ocx service install checks loginctl show-user <current-user> -p Linger before printing boot advice. Enabled linger is reported as already enabled. Disabled linger gets the loginctl enable-linger $USER hint; missing or unreadable diagnostics state the uncertainty and do not fail installation.
On macOS, ocx service status distinguishes a loaded Desktop login item from a valid enabled registration awaiting the next login. ocx status credits the latter in restart safety when the same Desktop app currently supervises the proxy. An incorrect plist label or command, a disabled entry, an unverified load probe, or changed process identity still cannot earn protection.
Custom Codex provider consent
Section titled “Custom Codex provider consent”ocx codex-provider status lists the provider IDs approved for the selected Codex home.
ocx codex-provider allow <id> asks before allowing URL-only management on start/sync;
--yes is explicit noninteractive consent for that named provider.
ocx codex-provider revoke <id> restores an unchanged managed URL and revokes permission.
These commands are local and do not require a running proxy. See
custom provider routing
for eligibility, restore behavior and transport preservation.

