Codex sandbox loses captured stdout/stderr from nested Node.js child processes writing via `process.stdout` / `process.stderr`
What version of Codex CLI is running?
codex-cli 0.121.0
What subscription do you have?
ChatGPT Plus
Which model were you using?
gpt-5.4
What platform is your computer?
Linux 6.6.87.2-microsoft-standard-WSL2 x86_64 x86_64
What terminal emulator and version are you using (if applicable)?
_No response_
What issue are you seeing?
In the Codex sandbox, captured stdout / stderr from a nested Node child process can be lost when the child writes through Node’s default process.stdout / process.stderr streams.
More specifically, if a Node process launches another Node process using captured pipe stdio, the child may exit successfully but the parent still receives empty captured output. Reproduced this with spawn, spawnSync, exec, execFile, and fork({ silent: true }). Default fork() is unaffected because its stdout/stderr are forwarded to the parent.
In the failing case, these child-side writes produced no captured bytes in the parent:
console.log(...)console.error(...)process.stdout.write(...)process.stderr.write(...)
This appears to be specific to captured pipe/socket-backed stdio in the sandbox. The following control cases worked correctly:
- top-level
nodeoutput is captured normally stdio: "inherit"shows the child output normally- file-backed stdio works normally
- fd-level writes such as
fs.writeSync(1/2, ...),fs.write(1/2, ...), andfs.createWriteStream({ fd: 1/2 })are captured correctly Node -> Python,Node -> bash, andPython -> Nodecapture work correctly
Inside the failing nested child, fd 1 and fd 2 are socket-backed, but Node initializes process.stdout / process.stderr differently than it does outside the sandbox. In the sandbox, they appear as plain Writable streams with no backing handle; outside the sandbox, they appear as Socket objects with a backing Pipe handle. This suggests a sandbox-specific issue in Node/libuv stdio initialization or handling for captured stdio.
This causes false negatives in automated verification: a tool may emit diagnostics or progress normally when run interactively, but Codex receives empty captured stdout / stderr.
What steps can reproduce the bug?
Run the following inside the Codex sandbox:
node -e 'const {spawnSync}=require("node:child_process"); const r=spawnSync(process.execPath,["-e","console.log(\"child-out\"); console.error(\"child-err\")"],{encoding:"utf8"}); console.log(JSON.stringify({status:r.status,stdout:r.stdout,stderr:r.stderr}));'
Observed result in the sandbox:
{"status":0,"stdout":"","stderr":""}
The same missing-output behavior reproduces with the other captured-stdio child_process APIs listed above.
Uploaded thread: 019d9fe5-7dd6-7890-a44a-6d4c366a376a
What is the expected behavior?
The parent process should receive the child Node process’s captured output normally:
{"status":0,"stdout":"child-out\n","stderr":"child-err\n"}
The same expectation applies to all the captured-stdio child_process APIs listed above.
Additional information
- Reproduced in the Codex sandbox on
WSL2 Ubuntu 24.04withNode v24.15.0. - The same repro behaves correctly outside the sandbox.
- This does not appear to be just a flush-on-exit race:
- adding short delays before exit did not help
- write callbacks still fired
.write()returned success- calling
.end()onprocess.stdout/process.stderrdid not restore captured output - Possibly separate observation: the sync variants (
spawnSync,execSync,execFileSync) can also surface anEPERMartifact even when the child appears to have run and exited withstatus === 0. This seems separate from the main missing-output bug, which also reproduces with async APIs (spawn,exec,execFile,fork({ silent: true })) where no such artifact is surfaced.
2 Comments
The same symptom is also reported in a comment on another issue (the issue itself looks unrelated): https://github.com/openai/codex/issues/8343#issuecomment-4013913734
Ran into the same some time ago and still exists in codex 0.135.0.
Failure appears to occur when node child is spawned with socket descriptor(s) stdout/err, rather than tty or file-fd (likely for stdin as well, but not tested).
strace in Codex sandbox shows:
| getsockname_error: PermissionError:[Errno 1] Operation not permitted
| getpeername_error: PermissionError:[Errno 1] Operation not permitted
Outside the Codex sandbox, the equivalent descriptor classification succeeds and identifies AF_UNIX descriptors, for example:
| getsockname(3, {sa_family=AF_UNIX}, [128 => 2]) = 0
| getsockname(3, {sa_family=AF_UNIX}, [110 => 2]) = 0
| getpeername(3, {sa_family=AF_UNIX}, [110 => 2]) = 0
Appears Node/libuv is unable to classify inherited stdio descriptors because the sandbox denies getsockname / getpeername / related socket introspection on already-open stdout/stderr descriptors. Once classification fails, Node does not create functional stdout/stderr stream handles, even though the descriptors themselves remain writable.
Seems that Codex sandbox should to allow (maybe narrowly, or by config), descriptor-introspection syscalls needed by Node/libuv for already-open stdio descriptors. Specifically, getsockname / getpeername / relevant getsockopt calls used to classify AF_UNIX/socketpair stdio should not be denied (return EPERM) for descriptors that are already open and already passed into the sandboxed process.