codex app-server proxy exits 0 with empty stdout/stderr instead of bridging stdio
What version of the Codex CLI are you using?
codex-cli 0.131.0
What platform is your computer?
Arch Linux, x86_64.
What issue are you seeing?
codex app-server proxy is documented by --help as:
Proxy stdio bytes to the running app-server control socket
In practice it silently consumes stdin, exits with status 0, and writes nothing to stdout or stderr. Passing the socket explicitly with --sock ~/.codex/app-server-control/app-server-control.sock behaves the same way.
This makes callers believe the proxy path succeeded even though no JSON-RPC response is ever returned. In Desktop-style launchers this turns into an app-server connection failure / initialize timeout even though the underlying app-server daemon is healthy.
Reproduction
With an app-server daemon listening on the default control socket:
codex --version
# codex-cli 0.131.0
body='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"proxy-repro","version":"0.0.0"}}}'
printf 'Content-Length: %d\r\n\r\n%s' "${#body}" "$body" >/tmp/codex-proxy-repro-in.log
codex app-server proxy \
</tmp/codex-proxy-repro-in.log \
>/tmp/codex-proxy-repro-out.log \
2>/tmp/codex-proxy-repro-err.log
echo $?
wc -c /tmp/codex-proxy-repro-in.log /tmp/codex-proxy-repro-out.log /tmp/codex-proxy-repro-err.log
Actual result:
0
183 /tmp/codex-proxy-repro-in.log
0 /tmp/codex-proxy-repro-out.log
0 /tmp/codex-proxy-repro-err.log
The same result happens when the socket is passed explicitly:
codex app-server proxy --sock ~/.codex/app-server-control/app-server-control.sock \
</tmp/codex-proxy-repro-in.log \
>/tmp/codex-proxy-repro-sock-out.log \
2>/tmp/codex-proxy-repro-sock-err.log
0
183 /tmp/codex-proxy-repro-in.log
0 /tmp/codex-proxy-repro-sock-out.log
0 /tmp/codex-proxy-repro-sock-err.log
Expected behavior
Either:
codex app-server proxyshould bridge Content-Length-framed stdio JSON-RPC to the app-server control socket and return theinitializeresponse, or- it should fail non-zero with a diagnostic explaining why it cannot proxy.
Evidence that the daemon/socket is healthy
A direct WebSocket upgrade against ~/.codex/app-server-control/app-server-control.sock, followed by a masked text frame containing the same initialize JSON-RPC payload, succeeds:
HTTP/1.1 101 Switching Protocols
{"id":1,"result":{"userAgent":"t/0.131.0 (Arch Linux Rolling Release; x86_64) ghostty/1.3.1-arch2 (direct-ws-repro; 0.0.0)","codexHome":"/home/shuv/.codex","platformFamily":"unix","platformOs":"linux"}}
So the bug appears isolated to the app-server proxy stdio-to-WebSocket bridge path, not to the daemon or the control socket.
3 Comments
Technical Solution
Fix: codex
app-server proxyExits 0 with Empty stdout/stderr Instead of Bridging stdioIssue Summary
Bug:
codex app-server proxyis documented as "Proxy stdio bytes to the running app-server control socket" but silently consumes stdin, exits with status 0, and writes nothing to stdout or stderr.Root Cause: The
app-server proxysubcommand delegates tocodex_stdio_to_uds::run(), which performs raw byte-level copying between stdin/stdout and a Unix Domain Socket. However, the app-server control socket expects WebSocket-framed messages, not raw bytes. When raw bytes are written to the socket, the WebSocket server either:Since
tokio::try_join!completes when stdin reaches EOF (the input file is fully consumed), the proxy exits 0 before any response could arrive — and no response ever arrives because the server never understood the input.Impact: Desktop-style launchers that use
codex app-server proxyas a stdio bridge for JSON-RPC communication with the app-server experience silent connection failures / initialize timeouts.Location:
codex-rs/cli/src/main.rs(handler) →codex-rs/stdio-to-uds/src/lib.rs(implementation)---
Root Cause Analysis
Current Code Path
The Mismatch
| Layer | What proxy sends | What server expects |
|-------|-----------------|---------------------|
| Transport | Raw UnixStream bytes | WebSocket over UDS |
| Framing | None (raw bytes) | WebSocket text frames |
| Protocol | Content-Length HTTP (if any) | JSON-RPC 2.0 over WebSocket |
The app-server control socket is a WebSocket server (as confirmed by the issue author's successful direct WebSocket upgrade test). The
codex_stdio_to_udscrate performs raw byte copying without any WebSocket framing or Content-Length parsing.Why It Exits 0
tokio::try_join!completes when both futures complete.copy_stdin_to_socketcompletes when stdin reaches EOF (the input file is fully consumed).copy_socket_to_stdoutmay still be waiting for data from the socket (which never comes because the server didn't understand the input). However, if the server closes the connection due to protocol error,copy_socket_to_stdoutalso completes, and the function returnsOk(()).---
Proposed Fix
Option A: Implement proper stdio-to-WebSocket bridge (Recommended)
Replace the raw byte-copy approach with a proper stdio-to-WebSocket bridge that:
File:
codex-rs/stdio-to-uds/src/lib.rsOption B: Quick fix - detect and report the protocol mismatch
If a full WebSocket bridge is out of scope, at minimum the proxy should:
File:
codex-rs/stdio-to-uds/src/lib.rs---
Files Changed
| File | Change |
|------|--------|
|
codex-rs/stdio-to-uds/src/lib.rs| Replace raw byte bridge with Content-Length + WebSocket bridge ||
codex-rs/stdio-to-uds/Cargo.toml| Addtokio-tungstenite,url,urlencodingdependencies ||
codex-rs/stdio-to-uds/tests/stdio_to_uds.rs| Add test for Content-Length framing round-trip |---
Risk Assessment
---
Solution developed through automated analysis. Happy to discuss further or provide PR if needed.
Thanks for the detailed repro. I don’t think this is a bug in the proxy contract as currently implemented, though the UX/help text could definitely be clearer.
codex app-server proxyis a raw byte tunnel to the app-server control socket. The Unix control socket itself expects a WebSocket HTTP Upgrade handshake followed by WebSocket frames; it does not accept MCP-styleContent-Length-framed JSON-RPC directly.That matches the current docs in
codex-rs/app-server/README.md:And the implementation in
codex-rs/stdio-to-uds/src/lib.rsonly copies bytes between stdio and the UDS. The server side then callsaccept_async(...)on that socket, so sending rawContent-Lengthbytes will fail the WebSocket upgrade path.So the direct WebSocket test succeeding is expected, and the proxy producing no JSON-RPC response for Content-Length input is also consistent with the current contract.
I opened a follow-up with a narrower reproduction that avoids the Content-Length framing ambiguity from this issue: #25846.
In that repro, the client sends the framing currently described in
codex-rs/app-server/README.md: WebSocket HTTP Upgrade plus a client-masked WebSocket text frame containinginitialize.Observed on macOS:
101 Switching Protocols, initialize response frame observed,initialize_ok=true~/.codex/packages/standalone/current/codex app-server proxy --sock <default control socket>: timeout, no101, no response framecodex app-server proxy --sock <default control socket>: timeout, no101, no response frameSo the new issue is meant as a WebSocket-framed proxy-path reproduction rather than a duplicate of the Content-Length repro here.