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 ↗

2 Comments

fal-works · 4 months ago

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

gitw-dev · 2 months ago

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.