Codex sandbox loses captured stdout/stderr from nested Node.js child processes writing via `process.stdout` / `process.stderr`

Open 💬 2 comments Opened Apr 18, 2026 by fal-works

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 node output 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, ...), and fs.createWriteStream({ fd: 1/2 }) are captured correctly
  • Node -> Python, Node -> bash, and Python -> Node capture 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.04 with Node 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() on process.stdout / process.stderr did not restore captured output
  • Possibly separate observation: the sync variants (spawnSync, execSync, execFileSync) can also surface an EPERM artifact even when the child appears to have run and exited with status === 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.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗