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.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗