Skip to content

Native account routing through a transparent loopback relay

Production relay verification

The production relay at commit 93e7ae0 passed native account smoke checks on 2026-09-14 using an immutable compiled snapshot. Both clients returned the expected final OK and a completed response event, with exit code zero and no timeout. Only each client's session base URL changed; existing accounts and model selection were preserved.

Client Native transport Observed model response Final output and completion
Claude Code 2.1.268 HTTP Messages HTTP 200 Passed
Codex CLI 0.154.0 Responses WebSocket Upgrade 101 Passed

Claude also requested HEAD /api/hello; Codex requested /models before its WebSocket upgrade. The real adapters emitted prompt observations for both clients. The checks used temporary empty working directories and a prompt requesting no tool use. Reports retained only endpoint/status counts and completion booleans, and scratch working directories were removed.

This verifies native account routing and actual response completion through the implementation. It does not establish tool-consumption speedups, every provider variant, or live API-key compatibility. Neither OPENAI_API_KEY nor ANTHROPIC_API_KEY was available; synthetic API-key fixtures cover forwarding, while live API-key checks remain unverified. The new observer remains experimental because the deterministic replay failed its speedup and settled-waste gates; native speedup remains unverified.

Earlier scratch routing probe

Tested 2026-09-14 with Claude Code 2.1.268 authenticated through claude.ai and Codex CLI 0.154.0 authenticated through ChatGPT. Authentication status was supplied through sanitized CLI status; no credential store or token value was read.

Both tests used an empty temporary working directory, a tiny prompt requesting only OK, no explicit model override, a 120-second process cap, discarded native-client stdout/stderr, and a scratch HTTP/WebSocket relay bound to 127.0.0.1. The relay streamed request and response bytes without inspecting or logging bodies. It recorded only method, destination path, status, transport, and header names. No durable client configuration was written.

Claude Code native subscription

Launch isolation:

  • --safe-mode disabled customizations, hooks, and MCP while retaining native auth and model selection.
  • --no-session-persistence prevented transcript persistence.
  • --tools "" removed tool definitions for the smoke.
  • Only ANTHROPIC_BASE_URL was changed, pointing at the loopback relay. The relay forwarded to https://api.anthropic.com.
  • No API key, auth token, OAuth token, provider, or model variable/flag was added or changed.

Observed traffic:

  1. HEAD /api/hello -> 200.
  2. Two POST /v1/messages?beta=true requests -> 200 for both.
  3. Claude exited 0, without timeout or signal.

Observed model-request header names:

accept, accept-encoding, anthropic-beta, anthropic-dangerous-direct-browser-access, anthropic-version, authorization, connection, content-length, content-type, host, user-agent, x-app, x-claude-code-session-id, x-stainless-arch, x-stainless-lang, x-stainless-os, x-stainless-package-version, x-stainless-retry-count, x-stainless-runtime, x-stainless-runtime-version, x-stainless-timeout.

Conclusion: Claude's existing claude.ai subscription credential and native model selection work through an ANTHROPIC_BASE_URL loopback relay to api.anthropic.com. The relay must preserve the authorization, Anthropic beta/version, and Claude session-id headers and the request bytes. This smoke selected HTTP streaming; it did not exercise Bedrock, Vertex, Foundry, custom gateways, or an Anthropic WebSocket path.

Codex native ChatGPT account

Launch isolation:

  • codex exec --ephemeral prevented session persistence.
  • The run used an empty non-repository directory, ignored execpolicy rules, disabled hooks for this invocation, and selected the read-only sandbox with approval_policy="never".
  • Only openai_base_url was changed with a session-only -c override pointing at the loopback relay. The relay forwarded to https://chatgpt.com/backend-api/codex.
  • No API key, auth token, provider, profile, model, or transport override was added or changed.

Observed traffic:

  1. Two authenticated GET /backend-api/codex/models?client_version=0.154.0 requests -> 200 for both.
  2. Codex selected WebSocket itself and requested GET /backend-api/codex/responses -> 101 Switching Protocols.
  3. Codex exited 0, without timeout or signal. The scratch harness discarded output and did not independently assert a completed model response.

Observed model-discovery request header names:

accept, authorization, chatgpt-account-id, host, originator, user-agent, version.

Observed Responses WebSocket request header names:

authorization, chatgpt-account-id, connection, host, openai-beta, originator, sec-websocket-extensions, sec-websocket-key, sec-websocket-version, session-id, thread-id, upgrade, user-agent, version, x-client-request-id, x-codex-beta-features, x-codex-routing-hint, x-codex-turn-metadata, x-codex-window-id.

Observed upgrade response header names:

cf-cache-status, cf-ray, connection, cross-origin-opener-policy, date, nel, referrer-policy, report-to, sec-websocket-accept, sec-websocket-extensions, server, set-cookie, strict-transport-security, upgrade, x-content-type-options, x-models-etag, x-openai-proxy-wasm.

Conclusion: for the built-in OpenAI provider under a native ChatGPT account, a session-only openai_base_url override routes model discovery and the client-selected Responses WebSocket through loopback while retaining native account headers and model/provider selection. This establishes routing and authentication feasibility. Codex can exit successfully without a model response, so production smoke tests must separately assert a terminal response and expected output. Production proxy mode must support /models plus WebSocket upgrade and bidirectional message payloads; forcing HTTP would break native transport parity.

Launcher implications

  • The two native subscription paths are viable without credential extraction or replacement: override only the active base URL, forward bytes and all end-to-end headers, and restore nothing because the override exists only in the launched child.
  • A launch-owned relay can correlate Claude by x-claude-code-session-id. Codex runtime traffic provides session-id, thread-id, x-codex-turn-metadata, x-client-request-id, and x-codex-window-id; preserve them and use only the minimum stable identifiers after fixtures establish lifecycle behavior.
  • Codex's upstream base includes /backend-api/codex; the relay must prepend that base path when forwarding the relative /models and /responses requests generated from the loopback openai_base_url.
  • Keep hook-only fallback for failed relay health, unsupported providers, or unavailable transport support. Provider-specific Claude cloud routes and Codex custom providers still require their own fixtures.
  • Initial parser-only attempts exited before contacting a model endpoint: Claude's variadic --tools consumed a trailing prompt until argument order was corrected, and codex exec rejected the top-level-only --ask-for-approval flag. Neither attempt made a model request.

Codex account-mode query

Codex 0.154.0's stable generated app-server schema supports account/read with { "refreshToken": false }. Its nullable account has a type discriminator of apiKey, chatgpt, or amazonBedrock. The launcher can reuse its configuration-reader connection and retain only this discriminator for built-in upstream selection. Null, unsupported, or malformed results leave account routing unverified.

A bounded native query verified the existing initialization contract and returned the sanitized discriminator chatgpt. No tokens were requested, no login or refresh flow was invoked, and no model request was made. The full account response and its personal metadata were discarded; temporary generated schemas and helper files were removed. This verifies the account-query seam, while live API-key forwarding remains a separate unverified gate.

Codex launch override ordering

Two isolated Codex 0.154.0 parser probes verified that exec accepts a final global -c override after its positional prompt, or immediately before a native -- sentinel. The later value for the same scalar key won in both cases. Original arguments can therefore retain their order while generated launch controls occupy the final configuration position.

Both probes selected nonexistent synthetic providers and stopped during local startup, before authentication, model discovery, inference, or MCP requests. They establish scalar ordering for these exec forms; they do not establish table-conflict behavior or compatibility with every native subcommand. Per-run permissions must still come from the original effective configuration before transport and wrapper controls are added.

Source-built session launcher smoke

The actual run <client> --observe proxy --json-report <path> -- ... command at f65e38f completed a minimal no-tool model turn on both installed clients, using their existing native accounts and original model/effort settings. Each run asserted the expected final response and a completed turn independently of process exit.

Client Active report mode Relay requests Relay failures Completed expected response Elapsed
Claude Code 2.1.268 proxy 3 0 yes 3.147 s
Codex CLI 0.154.0 proxy 2 0 yes 6.550 s

These are routing and completion checks, not performance comparisons. Both reports had zero registered routes because the smoke intentionally used no MCP tools. Codex reported disabled hook coverage, opaque native exec stream coverage, and lost completion tracking. Detected tracking loss disables correlated prediction for that launch while native forwarding continues. Actual MCP transfer is verified below; native matched-task benefit and live API-key use remain unverified.

Native MCP transfer verification

Actual native account turns now pass in off, hooks, and proxy modes on both clients. Each requested one synthetic read, received the expected result, completed the turn, and issued exactly one underlying MCP call. The fixture's initialization identified its client as Speculate, proving the request traversed the wrapper. Claude was tested at bdd4120; Codex passed after the configuration correction at d3391a7.

Client off hooks proxy Duplicate fixture calls
Claude Code 2.1.268 passed passed passed 0
Codex CLI 0.154.0 passed passed passed 0

The initial Codex attempt exposed a native key-path difference: quotes in -c mcp_servers."alias".command=... become part of the server name. Generated overrides now use verified bare segments and abstain for unsupported segments. A native configuration read confirmed exactly one wrapped alias and unchanged enabled state, tool allowlist, and approval requirement. A separate startup check initialized and listed the owned fixture against a local provider that stopped before inference. The corrected source then passed all three native Codex transfer modes.

The sanitized results retain the earlier failed Codex rows and the corrected results. These single samples prove ownership, result integrity, and duplicate-execution safety; they do not demonstrate prefetch benefit or a speed improvement. Hook delivery remains unverified, including Codex's reported trust restriction. Codex opaque native exec and observed correlation-loss limitations remain visible. Live API-key modes are unverified. The report's registeredRoutes value is sampled after child shutdown, so it is not a count of routes registered during the run.

Post-review source verification

At 02e951e, Claude 2.1.268 completed a native-account proxy-mode transfer with --strict-mcp-config, and Codex 0.154.0 completed a native-account proxy-mode transfer through its native transport. Both produced the expected final response and exactly one correct wrapper-owned MCP call. The strict-source regression tests separately verify that ambient registrations are excluded. These single samples are correctness evidence, not performance comparisons; see the sanitized final native results.