Skip to content

Server and Runtime Configuration

Server settings control how the local proxy listens, protects remote traffic, manages resources, and runs helper features around provider requests.

Field Type Default Meaning
port number 10100 Proxy listen port.
hostname? string "127.0.0.1" Bind address. A non-loopback bind requires a data-admission token, resolved from OPENCODEX_API_AUTH_TOKEN, then OCX_API_TOKEN_FILE, then the installed owner-only service-api-token — nothing has to be exported by hand. See Remote access.
proxy? string Outbound HTTP(S) proxy URL, ${ENV_VAR}, or "auto". Applied to HTTP_PROXY / HTTPS_PROXY only when those variables are unset; loopback remains in NO_PROXY. "auto" reads the Windows system proxy (WinINET ProxyEnable/ProxyServer, https= then http= entry) once at process start and logs the host it chose. On other platforms, or when the system proxy is off, SOCKS-only, or unreadable, it uses direct egress and says so. PAC/WPAD and live proxy changes are not followed; restart the service after changing the system proxy.
noProxy? string | string[] Hosts that bypass proxy, merged with inherited NO_PROXY and loopback entries. A string may use comma-separated NO_PROXY syntax or ${ENV_VAR}.
emptyCompletionRetry? boolean false Opt in to one identical Responses retry when a turn has no text or tool call, including a stream that ends before a terminal event. The retry may be billable. OCX_EMPTY_COMPLETION_RETRY=0 disables it without changing config; combo and routed-compaction turns remain excluded.
dropCodexSafetyBuffering? boolean false Remove optional client-facing hints from canonical Codex Responses passthrough: the two x-codex-safety-buffering-enabled / x-codex-safety-buffering-faster-model response headers, response.metadata events whose metadata type is safety_buffering, and top-level safety_buffering fields. Other headers, response data, policy refusals and failures are preserved. This does not disable provider safety enforcement or upstream buffering. Native codex.response.metadata.headers WebSocket metadata and /responses/compact are outside this filter.
stallTimeoutSec? number 300 Seconds without meaningful upstream progress (Responses and native Chat). Minimum 1.
oauthOpenBrowser? boolean true Whether a login may open a browser on the machine running the proxy. Absent and true both open, so an existing install is unchanged; only an explicit false declines. Decline when you need the authorization link in a different browser profile, or when the dashboard is not on the proxy’s machine — the login still starts and the URL is still returned and displayed. POST /api/oauth/login and POST /api/codex-auth/login accept a per-request openBrowser boolean that overrides this, and the dashboard exposes the same choice beside the login button. Device-code flows never open a browser either way.
connectTimeoutMs? number 200000 Per-attempt DNS/TCP/TLS/final-header deadline; it ends before body generation.
shutdownTimeoutMs? number 5000 Graceful drain deadline before active turns are aborted.
websockets? boolean false Advertise and admit the client-facing Responses WebSocket path. False keeps clients on HTTP/SSE; it does not disable an eligible canonical ChatGPT upstream WS optimization. Complete-input requests may reuse an upstream connection within the same selected credential, account, thread and turn; changed handshake policy or missing identity keeps requests on separate connections. This does not trim HTTP input or create previous-response IDs.
codexNativeSteering? boolean false Experimental, native-only mid-turn steering on the Responses WebSocket endpoint. Requires websockets: true, a compatible upstream/client, and a pinned account/model/tool surface. Validated generation settings can change in explicit saved-result continuations. Does not enable translated models or HTTP fallback. See native steering.
codexNativeInjection? boolean false Experimental saved function-result injection on compatible native multi-agent WebSocket turns. Requires websockets: true, explicit multi_agent.enabled, and an eligible provider. Separate from steering; no automatic tool rerun or recovery create. See native injection.
corsAllowOrigins? string[] [] Additional exact origins allowed by CORS. Loopback origins are always allowed. Authority-based browser extension origins such as chrome-extension://<extension-id> are supported; * is not a wildcard. Firefox and Safari regenerate the extension UUID (per install / per browser launch), so update the entry when the origin changes.
apiKeys? OcxApiKey[] [] Generated ocx_… credentials accepted by management and data-plane auth on non-loopback binds. Dashboard-managed.
storageCleanupPolicy? StorageCleanupPolicy disabled Opt-in archived-session cleanup policy. Never enabled implicitly.
appOwnedMemoryBudgetMb? number 256 Cap in MiB for evictable app-owned logs, caches, blobs, and continuation payloads. Range 64–4096; not an RSS cap.
codexAutoStart? boolean true Let the Codex shim run ocx ensure before launching Codex. False makes ensure a no-op.
codexShimAutoRestore? boolean true Restore an installed shim after a completed external Codex update replaces it. Environment opt-out: OPENCODEX_CODEX_SHIM_AUTO_RESTORE=0.
codexDesktopAuthless? boolean false Opt-in authless Codex Desktop routing on a loopback bind: inject the dedicated opencodex provider with requires_openai_auth = false so Desktop opens without a ChatGPT login. Ignored on non-loopback binds. ocx system settings --desktop-authless on. See Codex integration.
codexClientCompaction? boolean false Opt into Codex client-side compaction on an authenticated loopback bind. Uses the dedicated opencodex provider identity with requires_openai_auth = true, preventing new routed compactions from storing OpenCodeX-owned ocx1: state. codexDesktopAuthless takes precedence when both are enabled and keeps requires_openai_auth = false. V2 sub-agent routing is unchanged. ocx system settings --client-compaction on. See Codex integration.
resetCreditAutoRedeem? { enabled?: boolean; leadTimeMinutes?: number } off Opt-in: redeem the main Codex account’s soonest-expiring reset credit leadTimeMinutes (1–60, default 10) before it expires. Every attempt re-reads the upstream credit list first and skips when the credit is gone (for example, redeemed by hand); the redeem_request_id is journaled in $OPENCODEX_HOME/reset-credit-auto-redeem.json before the call so a crash replays the same idempotent request instead of spending a second credit. Servers sharing this configuration directory coordinate reservations and settlements so one process does not replace another’s request record. Logs carry a hashed account key only.
syncResumeHistory? boolean true Reversible Codex App history compatibility. Original metadata is backed up and restored by ocx stop / ocx restore.
shadowCallIntercept? { enabled?: boolean; model?: string; sourceModels?: string[] } off Redirect recognized Codex helper/shadow calls to a chosen model while preserving the request’s configured reasoning effort. The default source prefix is gpt-5.6-luna; older clients through 0.144.x used gpt-5.4-mini, which sourceModels can restore.
webSearchSidecar? OcxWebSearchSidecarConfig on when usable Web-search sidecar options.
visionSidecar? OcxVisionSidecarConfig on when usable Image-description sidecar options.
images? OcxImagesConfig automatic OpenAI selection Standalone Images relay options for Codex image_gen.

While the canonical ChatGPT upstream WebSocket waits for the first Responses event after sending the create frame, it watches for liveness rather than a fixed deadline. When the socket supports protocol pings, the proxy pings it every 15 seconds. Any inbound frame — quota, response metadata, or a pong — resets a 90-second silence clock, so a socket without ping support still stays alive on its own frames, and only 90 seconds with nothing at all settles the request as an HTTP 504 with an upstream_no_response error. A slow but alive origin therefore waits for the client’s own deadline or for connectTimeoutMs (default 200s), whichever comes first; a connect timeout that fires after the create frame was sent settles as the same 504. A socket that closes or errors before the first Responses event settles as an HTTP 502 with upstream_closed_before_response. These statuses are never retried inside the proxy — the frame may already be executing upstream, so the client applies its own retry policy exactly as it would when connected to the backend directly. Once the response has started, a later drop surfaces inside the stream as before. stallTimeoutSec is unrelated to this window.

An ordinary HTTP send has a third case. When the connection dies before any response header arrives, the proxy cannot tell whether the model already processed the request, so it refuses to send it again and answers HTTP 429 with upstream_reset_replay_refused. The status is deliberate: a 5xx here is an instruction to most clients, including Codex, to send the whole turn again, which is the duplicate the refusal exists to prevent. No Retry-After is attached, and the proxy performs no key rotation, account failover or same-target replay on it, nor does it record the refusal as rate-limit or quota evidence against the credential it was holding. Tool-call side requests such as vision and web search are replayed normally, because repeating them cannot duplicate a turn.

noProxy accepts either a comma-separated string or an array. Both forms add entries without replacing an inherited NO_PROXY:

{ "proxy": "http://proxy.corp:8080", "noProxy": "internal.example,10.0.0.0/8" }
{ "proxy": "http://proxy.corp:8080", "noProxy": ["internal.example", "10.0.0.0/8"] }

If an older development build changed resume-history metadata before backup support existed, run ocx recover-history --legacy-openai --yes to force native-provider recovery. It force-relabels every user-message opencodex row, including legitimate dedicated-provider history; review the full-scope warning in the lifecycle reference before running it.

Native Chat also uses stallTimeoutSec while waiting for upstream output. Nonempty text, reasoning, refusal, tool updates, and finish frames renew the allowance; keepalive comments, role-only frames, and usage alone do not. Waiting for a slow client to read pauses the allowance. A stall produces upstream_stall_timeout: an error frame for streaming clients, or HTTP 502 for non-streaming clients. Cancellation before a terminal result returns a cancellation error instead of a successful partial answer. Buffered Chat results accept both LF and CRLF SSE framing, including multiline data.

The main Codex account row may include quotaRefresh when a quota fetch was attempted. This describes that fetch, not remaining quota, model access or permission to retry. Cached reads and rows without a fetch may omit it; absence does not mean success. A null quota value means unavailable, not zero quota.

To request fresh data and display only the diagnostic in PowerShell:

Terminal window
$quotaReport = ocx account list openai --quota --refresh --json | ConvertFrom-Json
$quotaReport.accounts |
ForEach-Object { if ($_.quotaRefresh) { $_.quotaRefresh } } |
ConvertTo-Json -Depth 3

If no diagnostic is present, this projection produces no diagnostic object. Share only these fields when comparing network modes, rather than the full account list.

quotaRefresh.status Meaning
ok The fetch completed and a quota object was parsed.
not_reported The response contained no usable quota object.
http_error The upstream returned an HTTP failure; httpStatus contains its status code.
timeout The quota fetch timed out.
network_error The request failed before a classified HTTP response.
invalid_response The response was not a usable quota document.
internal_error An internal refresh step failed.

Only http_error includes httpStatus. Other statuses do not imply HTTP 0 or an account entitlement problem.

The running proxy service fetches quota. It uses its own environment, not the interactive shell that later runs ocx account list. Configure the service’s proxy setting or environment, then restart it; changing variables in another terminal does not update an already running service.

An unset proxy leaves inherited proxy variables unchanged. An explicit HTTP(S) proxy URL fills HTTP_PROXY and HTTPS_PROXY only where they are unset. "proxy": "auto" reads the Windows static WinINET proxy once at startup; existing proxy environment variables take precedence. Auto discovery does not resolve PAC/WPAD, SOCKS-only settings or live proxy changes. Use a supported static HTTP proxy setting or an explicit HTTP(S) proxy URL when needed.

Compare the diagnostic on the same machine and account under the two network modes. A successful TUN test alone does not identify why the service’s HTTP proxy path failed, and does not establish a general fix.

The default 127.0.0.1 bind is loopback-only. A non-loopback address such as 0.0.0.0 or a tailnet IP requires token authentication on both /api/* and the data plane.

You do not have to produce that token. ocx service install provisions one on a non-loopback bind, in this order: OPENCODEX_API_AUTH_TOKEN from the installing shell, then an existing owner-only service-api-token file, then 32 fresh random bytes. The result is written 0600 and the launch wrapper (launchd plist, systemd unit, Windows wrapper) reads the file at start, so the value never enters a service definition or argv. A foreground ocx start applies the same precedence — environment, then OCX_API_TOKEN_FILE, then the installed service-api-token — so it binds a non-loopback hostname without an exported token too.

A management admin token is refused in either place it can appear — the environment variable or a reused service-api-token file — and the message names the remedy for that place: unset the variable, or delete the file and run ocx service repair. Both checks run before the loopback short-circuit, because the launch wrapper reads the file into OPENCODEX_API_AUTH_TOKEN whatever the hostname, so an admin-token file fences the management API closed even on a loopback bind. ocx status reports that state as admin-collision (file) on a hub.

Setting the variable yourself is still supported for an operator who wants to own the value:

Terminal window
export OPENCODEX_API_AUTH_TOKEN="your-secret-token"
ocx start

Clients should send:

x-opencodex-api-key: your-secret-token
Endpoint Authorization: Bearer x-opencodex-api-key x-api-key
/v1/responses not accepted required not accepted
/v1/chat/completions not accepted required not accepted
/v1/messages accepted accepted accepted
/v1/messages/count_tokens accepted accepted accepted
/v1/models accepted accepted accepted

Responses and Chat Completions reserve Authorization for possible Codex Direct passthrough, so only the dedicated admission header is accepted there. Dashboard-generated apiKeys may replace the environment token after startup; candidates are compared in constant time.

Messages and count_tokens keep accepting all three admission forms for routed-client compatibility. Native Anthropic passthrough is stricter on a non-loopback bind: proxy admission must use x-opencodex-api-key, while Authorization and x-api-key are reserved for Anthropic credentials. Any proxy admission secret placed in those provider headers is removed before forwarding.

Local clients that cannot receive the token

Section titled “Local clients that cannot receive the token”

A remote bind requires a credential from every caller, including local ones. That breaks a specific case: a codex app-server launched by a host process that resolves the Codex entrypoint directly (require.resolve('@openai/codex/bin/codex.js')) never passes through the generated codex shim, so it never inherits OPENCODEX_API_AUTH_TOKEN and every model call fails with 401 before a stream opens.

unauthenticatedLoopbackListener opens a second listener bound to 127.0.0.1 that admits without a credential. The main listener is untouched — remote callers still need the token.

{
"hostname": "0.0.0.0",
"port": 10100,
"unauthenticatedLoopbackListener": { "enabled": true, "port": 10200 }
}

ocx sync then writes base_url = "http://127.0.0.1:10200/v1" into the managed Codex provider block and omits the auth header, so a directly spawned app-server works without any credential plumbing.

When you set port, it must differ from the proxy port. It is never OS-assigned: an ephemeral port would change across restarts while already-running app-servers kept the previous base_url.

Omitting port selects the companion form — the listener binds the proxy port on 127.0.0.1:

{
"hostname": "100.76.170.81",
"port": 10100,
"unauthenticatedLoopbackListener": { "enabled": true }
}

Remote clients dial 100.76.170.81:10100 with a credential; local processes dial 127.0.0.1:10100 without one. That is the address every local integration already writes, so ocx claude, Claude Desktop, Cursor and the system-env injection keep working on a host whose public bind they cannot reach. The companion form is accepted only when hostname is a specific non-loopback, non-wildcard address: on 127.0.0.1, localhost or 0.0.0.0 the public listener already holds that loopback address, so OpenCodex refuses the pair at write time and at startup rather than failing the second bind. On those binds you do not need the listener at all — a loopback bind already admits local callers.

With a port set, the local integrations follow the listener: ocx claude, the system-env injection, the Claude Desktop profile, the Cursor gateway value and the routed vision helper all write http://127.0.0.1:<listener port>, the same port ocx sync writes into Codex. In the companion form those same integrations keep writing the proxy port, which is where the companion socket is.

Restart the proxy after changing this field, in either form. The sockets are bound once at startup and the exported client values are written from the resolved port, so a running proxy keeps its previous answer — on a ported listener that is the difference between a served request and a 404 from the listener.

On a runtimeRole: "hub", this field is also the gate on whether the hub rewrites its own local client configuration. With the listener off, ocx sync, ocx ensure and ocx restore back skip the hub’s own Codex/Grok/Claude writes and say so, naming unauthenticatedLoopbackListener rather than the clientIntegrations toggle.

The listener serves only POST /v1/responses, its WebSocket upgrade, POST /v1/responses/compact, POST /v1/messages (the Anthropic wire Claude Code and Claude Desktop speak), POST /v1/chat/completions (the OpenAI chat wire Cursor and the vision helper speak), POST /v1/alpha/search (the native Codex web-search relay), GET /v1/models, and the realtime voice surface: the standalone WebSocket upgrades, WebRTC call creation (POST /v1/live, POST /v1/realtime/calls), and the keyed sideband join upgrades (/v1/live/{callId}, /v1/realtime/calls/{callId}, /v1/realtime?call_id=). Everything else, including /api/*, /healthz, /readyz and the dashboard, returns 404 — local management reads such as ocx claude’s discovery call go to the authenticated management surface with a management credential, never here.

Remote use does not require a remote bind. Keep loopback and forward it:

Terminal window
ssh -L 20100:localhost:10100 you@remote

Any local port works. Requests whose Host resolves to localhost, 127.0.0.1, or ::1 remain loopback regardless of port, so http://localhost:20100/v1 works. Set that base URL in the client; ocx writes only the default local 127.0.0.1 address into managed client config.

Provider OAuth callbacks listen on a fixed remote port. Log in on the remote machine or forward that port too:

Terminal window
ssh -L 20100:localhost:10100 -L 1455:localhost:1455 you@remote

If a registered callback port is already in use and the login surface offers manual input, OpenCodex keeps the registered redirect URI and still returns the provider authorization URL. Complete the provider login, then paste the final redirect URL from the browser address bar or the authorization code into OpenCodex. The pending flow preserves state and PKCE validation. Callers without manual input still fail closed.

Stored account emails are masked everywhere they leave the proxy — the dashboard account lists, GET /api/codex-auth/accounts, GET /api/oauth/status, and the ocx status logins section all show p***n@example.com rather than the address on file.

Field Type Default Meaning
privacy.maskEmails? boolean true Set to false to show stored account emails in full. Absent, true, and any malformed value all keep masking, so only a deliberate false reveals an address.

Turn it off when you run many accounts on a machine you control and cannot tell them apart from masked forms. Read the flag as a disclosure decision rather than a display preference: management is not always loopback, so on a hub configured with remoteGui an unmasked address reaches every management principal that can reach the hub, not only someone sitting at the machine. The flag moves only the email field — tokens, refresh tokens, and account identifiers stay redacted either way.

{ "privacy": { "maskEmails": false } }

ocx config set privacy.maskEmails false fails with config parent path not found until the block exists, because config set walks into existing objects and never creates them. Write the whole object instead — ocx config set privacy '{"maskEmails":false}' — or add the block to config.json by hand.

storageCleanupPolicy is disabled by default. When enabled, it runs on startup, daily, weekly, or manual after archived bytes exceed trigger.archivedBytesOver. It selects oldest archives toward either target.reduceToBytes or target.removeOldestPercent. mode defaults to quarantine; use permanent only as an explicit destructive choice. The policy persists lastRun and nextRun. Configure it on the Storage page or with GET/PUT /api/storage/cleanup-policy; trigger a manual run with POST /api/storage/cleanup-policy/run.

Quota-reset notifications (quotaResetNotify)

Section titled “Quota-reset notifications (quotaResetNotify)”

Off by default. When the section is absent, no detection runs, no timer starts, and no state file is written.

Enable it to be told when a usage window resets — both the scheduled rollover you can predict and an out-of-band reset you cannot:

{
"quotaResetNotify": {
"enabled": true,
"webhookUrl": "https://hooks.slack.com/services/...",
"kinds": ["scheduled", "surprise"],
"pollSeconds": 900
}
}
Field Default Meaning
enabled false Master switch. Also requires at least one sink, below.
kinds both scheduled (the deadline passed) or surprise (quota returned early).
pollSeconds 900 Idle poll interval; floor 600. 0 observes live traffic only.
webhookUrl https POST target for the event JSON. Treated as a secret.
allowPrivateNetwork false Permit a loopback or private-network webhook target.
timeoutMs 5000 Webhook timeout.
command Argv array run with the event JSON on stdin.

enabled: true with neither webhookUrl nor command resolves to off: an enabled subsystem with nowhere to deliver is a misconfiguration, not a half-on state.

The floor is 600 seconds because a faster poll cannot see anything new: observation is bounded by the 10-minute per-account cache, so a shorter interval only adds load to a quota endpoint that rate-limits. A configured value is adopted on the next tick without a restart.

Set pollSeconds to 0 only if you accept that a reset happening while the proxy is idle is noticed on the next request rather than when it happens. The poll exists because the overnight case is the one worth knowing about.

{
"type": "quota_reset",
"kind": "surprise",
"scope": "codex",
"accountTag": "k3f9x2ab",
"window": "weekly",
"percentBefore": 96,
"percentAfter": 4,
"previousResetAt": 1772000000000,
"resetAt": 1772400000000,
"detectedAt": 1771900000000
}

accountTag is a per-install salted hash, not an account identifier: it distinguishes your accounts from each other without telling the receiver who they are. No email, token, path, or URL is ever included.

detectedAt is when the proxy NOTICED, not when the reset happened. Observation is bounded by the 5-minute provider cache and the 10-minute per-account cache, so the reset instant can only be bracketed between two observations.

webhookUrl is a credential — for Slack and Discord, holding the URL is sufficient to post — so it is redacted by ocx config show and excluded from ocx config export.

A webhook target that resolves to a private or loopback address is refused unless you set allowPrivateNetwork: true. The proxy can reach hosts your browser cannot, including cloud metadata endpoints, so the default assumes an external receiver.

webhookUrl must use https. The payload and the URL itself are both sensitive, and an http target would put them in cleartext; an http value is rejected when the config is written rather than downgraded silently.

A redirect is refused rather than followed. The destination check above validates the URL you configured, so following a 3xx would deliver the payload somewhere unvalidated — a public endpoint could bounce the POST to loopback or a metadata address. Configure the final URL directly; a redirected delivery reports blocked-destination.

command is an argv array and is never passed through a shell, so its values cannot become a shell-injection surface. Delivery is attempted once; there is no retry.

Read recent detections with ocx provider resets or GET /api/quota-resets.

These settings govern /v1/messages, /v1/messages/count_tokens, the ocx claude launcher, and the Claude dashboard page.

Key Type Default Description
claudeCode.bodyStallSec? number 90 Native-passthrough body inactivity budget in seconds while a read is pending, not total duration. Minimum 1; exactly 0 disables.
claudeCode.bodyMaxBytes? number 67108864 Cumulative native-passthrough body cap for streamed and buffered responses. Exactly 0 disables.
claudeCode.compatibility? "shadow" | "enforce" unset Optional compatibility admission for translated /v1/messages requests. shadow records unsupported features and continues; enforce returns an Anthropic-shaped 400 before inference. Native Anthropic passthrough remains unchanged.
claudeCode.authMode? "proxy" | "subscription" auto How launch handles ANTHROPIC_AUTH_TOKEN. Auto detects auth each launch; an explicit value is never overridden.
claudeCode.authModeMigratedAt? string unset Internal one-time upgrade marker. Do not set manually.
claudeCode.classifierModel? string unset Explicit target for Claude Code Auto Mode classifier turns, as a qualified provider/model (for example RelayA/claude-opus-5). Auto Mode sends bare safety checks such as claude-opus-5 with no provider, so without this they fall through to defaultProvider — which may not speak Anthropic at all. Nothing is inferred automatically: only a target you declare here is used.
claudeCode.classifierFallbacks? string[] unset Ordered classifier targets used when classifierModel is not set. Same qualified provider/model form; the first usable entry wins. An explicit modelMap entry for the classifier model still outranks both.
claudeCode.subagentEffort? "low" | "medium" | "high" | "xhigh" | "max" inherit Effort written to generated ~/.claude/agents/ocx-*.md; separate from Codex guidance and proxy caps. Restart through ocx claude to regenerate.

The compatibility policy applies to Claude Code, Desktop and other clients using translated Messages, including ?beta=true and non-streaming requests. Every translated target uses the same conservative policy, including Anthropic and native Responses adapters. It rejects document content, thinking/redacted-thinking replay, hosted search and execution tools, tool-search references, active deferred loading, strict tools, non-default caller modes, structured-output formats, explicit service-tier intent, MCP connector features, context management, containers, inference placement and unsupported protocol fields or blocks.

Unset preserves legacy translation. Cache hints, tool input examples and ordinary thinking/effort settings are deliberately admitted with possible degradation: this setting does not guarantee cache breakpoints or TTL, retained examples, exact thinking budgets or lossless translation. Beta headers alone are not validated for feature support. Shadow evidence contains only fixed protocol codes and derived reasons, retained in request logs and usage.jsonl and restored on restart. An invalid non-unset mode returns a fixed 503 configuration error on translated Messages. Configure the value in config.json and restart the proxy to load it; there is no dedicated GUI setter. Count-tokens and direct Responses/Chat APIs are outside this policy; successful token counting does not imply Messages admission. This setting does not add a global authorization boundary.

Auto auth selects subscription when stored Claude auth is found, proxy when none is found, and subscription with a warning when detection is inconclusive. See Claude Code auth mode.

Codex uses small helper models for tasks such as titles and commit messages. Enable shadowCallIntercept to redirect recognized source-model prefixes to another configured model. The replacement keeps the request’s configured reasoning effort. Set sourceModels only when a client uses different helper ids. Interception is model-based: every request whose bare model id matches sourceModels can be redirected, including normal request_kind: "turn" requests. x-codex-turn-metadata does not exempt a matching request.

{
"shadowCallIntercept": {
"enabled": true,
"model": "gpt-5.5",
"sourceModels": ["gpt-5.6-luna"]
}
}
Field Type Default Meaning
provider? string automatic OpenAI selection Explicit custom API-key openai-responses provider for /v1/images/generations and /v1/images/edits. Registry-managed ids are rejected.
timeoutMs? number 300000 Whole-request timeout for one standalone Images request.

Explicit selection fails closed when the provider is missing, disabled, incompatible, or lacks a usable key; it never falls back to another paid upstream. The endpoint must implement the OpenAI Images API paths and response shape expected by Codex.

webSearchSidecar (OcxWebSearchSidecarConfig)

Section titled “webSearchSidecar (OcxWebSearchSidecarConfig)”
Field Type Default Meaning
enabled? boolean on when usable Master switch.
backend? "openai" | "anthropic" | "xai" | "gemini" | "exa" openai Explicit wins; unset always resolves to openai. anthropic and xai run only when explicitly configured; gemini and exa remain reserved until their executors ship.
model? string backend-dependent gpt-5.6-luna for OpenAI, claude-sonnet-5 for Anthropic, or grok-4.6 for xAI. Legacy explicit gpt-5.4-mini migrates on start.
exaApiKey? string none Operator key for the exa backend. Write-only: management reads never return the stored value.
xSearch? object omitted xAI-only opt-in for hosted x_search: enabled, mutually exclusive allowedXHandles / excludedXHandles arrays (maximum 20), and ISO fromDate / toDate (YYYY-MM-DD).
reasoning? string low Sidecar effort. minimal is rejected with web search.
maxSearchesPerTurn? number 3 Real searches allowed per main-model turn.
routedModelStallTimeoutMs? number 200000 Config-file-only routed-model raw-body inactivity deadline. Integer 1–2147483647; every non-empty chunk resets it.
timeoutMs? number 60000 Deadline for one hosted search.

The OpenAI backend requires a ChatGPT login and enabled ChatGPT forward provider. Claude-inbound routed replays inject main ChatGPT auth into the internal request. The Anthropic backend uses the active stored credential from an enabled Anthropic OAuth provider. An explicitly selected Anthropic backend with no usable account fails closed instead of falling back. The Anthropic executor uses its native web_search_20250305 tool. The xAI backend requires a usable stored Grok OAuth account, uses hosted web_search, and adds hosted x_search when xSearch.enabled is true. Malformed xSearch management input returns 400; a malformed persisted block fails closed during planning. The gemini and exa lanes never activate from credential discovery or fallback; the operator must select them explicitly. exaApiKey is accepted on writes but omitted from management responses.

Four clocks govern search: base stallTimeoutSec, connectTimeoutMs, routed-model inactivity, and hosted-search timeout. The effective bridge watchdog is the maximum plus 30 seconds. Routed stall is an inactivity guard, not a total generation deadline.

Field Type Default Meaning
enabled? boolean on when usable Master image-description switch.
backend? "openai" | "anthropic" auto Explicit wins; unset prefers a usable stored Anthropic OAuth credential, else openai.
model? string backend-dependent gpt-5.6-luna for OpenAI or claude-sonnet-5 for Anthropic.
maxDescriptionsPerTurn? number 8 New description cache misses admitted per main turn. 0 disables calls; invalid values use default.
timeoutMs? number 45000 Sidecar fetch timeout. Integer 1–2147483647.

Vision activates only for images sent to a model in its provider’s noVisionModels. OpenAI has the same login/forward requirements as search; explicitly selected Anthropic fails closed without a usable credential. Successful data: descriptions use a bounded cache keyed by backend, model, detail, image bytes, and normalized message context. Hits and same-turn duplicates do not consume the limit. Remote https: images and failed or empty descriptions are not cached.

Anthropic OAuth sidecars reuse opencodex’s existing Claude Code OAuth fingerprint. Soak-test the intended account and workload.

runtimeRole defaults to standalone. A hub uses hub.managementPublicOrigin, loopback-only hub.managementIngress (enabled:false when absent), and exact remoteGui.allowedTailscaleUsers (empty when absent). A client data key lives in service-api-token, never config.json; rotation may temporarily create service-api-token.prev. Usage stores are not mirrored.

Key Type Default when absent What it does
hub.managementPublicOrigin string unset The canonical browser-reachable management origin a hub advertises, for example the HTTPS origin Tailscale Serve prints. It is what /readyz reports as managementUrl while runtimeRole is hub; with it unset the hub falls back to whatever origin each request arrived on, so a client behind a different frontend can be handed an address it cannot reach.
hub.dataPublicOrigin string unset The canonical origin a remote client should dial for the data plane, for example the HTTPS origin a TLS frontend publishes in front of the tailnet bind. Advisory only: it is never a bind address and changing it moves no socket. ocx hub invite prints it as the positional URL of the ocx connect line, falling back to http://<hostname>:<port> — which is a LAN/tailnet address a remote machine may not be able to reach over TLS, so set this on any hub with a frontend. Unlike most optional keys it is not silently dropped when malformed: a typo is rejected at write time, because falling back to the bind address is exactly what the field exists to avoid.
hub.managementIngress {enabled:false} or {enabled:true, port} {enabled:false} An extra management-only listener for a local HTTPS frontend. The hostname is not configurable: when enabled the socket always binds 127.0.0.1, and only GUI, session-bootstrap, and management API routes are admitted. Data-plane routes are rejected before dispatch.
remoteGui.allowedTailscaleUsers string[] [] (empty — nobody) Exact Tailscale login identities allowed to be issued an automatic remote GUI session. The Tailscale-User-Login header is trusted only on the separate management ingress; an empty list means no remote identity can mint a session, which is the safe default rather than an oversight. Identities are compared exactly, so a typo silently denies access.
remoteGui.allowInsecureHttp boolean unset Retired — has no effect. It once permitted a one-time pairing exchange over non-loopback plaintext HTTP. A pairing grant now crosses loopback or authenticated HTTPS only. The key is still parsed so an existing config.json keeps loading (the schema is strict, and dropping the key outright would make an older config fail to load entirely); a persisted true is reported once and then ignored. Remove it from your config.

A hub that is reachable from a browser needs hub.managementPublicOrigin and at least one entry in remoteGui.allowedTailscaleUsers. Setting the origin without the user list produces a hub that advertises itself correctly and then refuses every session; setting the user list without the origin produces sessions pointed at whichever origin the request happened to use.

dataPublicOrigin and managementPublicOrigin are two independent advertisements, and on a real deployment they are two different sockets: management is the loopback-only ingress published on 443, data is the tailnet bind published on its own HTTPS port. They are the two halves of what ocx hub invite prints, and managementPublicOrigin is the stricter of the two — a pairing grant records it as the grant’s own server origin and the exchange compares against it, which is why ocx hub invite --management-url can only confirm the configured value and refuses one that differs. --data-url really is an override, because nothing is bound to it. With neither dataPublicOrigin nor --data-url set, invite falls back to the bind address — and on a loopback or wildcard bind, where that would resolve to this machine’s own loopback, it refuses rather than advertising an address the other machine cannot use.

A hub that serves its own local clients also sets unauthenticatedLoopbackListener. Its port-less companion form is what makes a hub a single-port deployment, and it is refused on a loopback or wildcard hostname, where the public listener already holds 127.0.0.1:<port>.

codexNativeSteering and codexNativeInjection enable separate, default-off native WebSocket control paths. See the canonical guide for supported steering routes and settings, typed result and approval continuations, and confirmation deadlines and retained context.