Windows Codex Desktop: shell_command tool drops stdout entirely; only stderr reaches the model
What version of the Codex App are you using (From “About Codex” dialog)?
26.519.5221.0
What subscription do you have?
Pro
What platform is your computer?
Microsoft Windows NT 10.0.26200.0 x64
What issue are you seeing?
On Windows Codex Desktop, the functions.shell_command tool silently discards every child process's stdout. Only output that arrives on stderr (or PowerShell's Error stream) is reflected in the tool result that the model sees.
The user-visible symptom is "I can briefly see command output flash on screen, but it disappears and the AI says it can't read anything". That is exactly what's happening: the live UI preview shows stdout chunks streaming in, but the final tool_result body delivered to the model contains only stderr.
Cross-runtime reproduction matrix — I tested every reasonable output channel on PowerShell 7, Windows PowerShell 5, cmd.exe, Python 3.12, and Node 20:
| Runtime | Channel / cmdlet | Visible to model? |
| --- | --- | --- |
| PowerShell | Write-Output (success / stdout) | ❌ |
| PowerShell | Write-Host (host) | ❌ |
| PowerShell | [Console]::Out.WriteLine(...) (stdout) | ❌ |
| PowerShell | [Console]::Error.WriteLine(...) (stderr) | ✅ |
| PowerShell | Write-Warning | ❌ |
| PowerShell | Write-Verbose -Verbose | ❌ |
| PowerShell | Write-Debug -Debug | ❌ |
| PowerShell | Write-Information -InformationAction Continue | ❌ |
| PowerShell | Write-Error (error) | ✅ |
| cmd.exe | echo xxx (stdout) | ❌ |
| cmd.exe | echo xxx 1>&2 (stderr) | ✅ |
| Python | print() / sys.stdout.write() | ❌ |
| Python | sys.stderr.write() | ✅ |
| Node | console.log() / process.stdout.write() | ❌ |
| Node | console.error() / process.stderr.write() | ✅ |
The pattern is exact: anything on the OS-level stdout pipe is dropped; anything on the OS-level stderr pipe is preserved. This is consistent across cmd, pwsh, python, and node — the bug cannot be in any individual shell's stream-routing logic.
This is not a sandbox/spawn failure (children all exit SUCCESS per ~/.codex/.sandbox/sandbox.log), and it is not an antivirus / PowerShell / PATH / pwsh.cmd wrapper / ExecutionPolicy issue. All those have been independently verified and ruled out via cross-runtime testing.
What steps can reproduce the bug?
Minimal reproducer
In any working directory, ask Codex to run this single command:
Write-Output 'stdout-line'
[Console]::Error.WriteLine('stderr-line')
Actual tool result body delivered to the model:
stderr-line
Expected tool result body:
stdout-line
stderr-line
The stdout-line row is rendered momentarily in the live TUI (the user sees it flash), but it never arrives in the model's tool_result. Subsequent model turns therefore reason as if stdout was empty.
Equivalent reproducers in other runtimes
# cmd.exe — only the second line will reach the model
cmd /c "echo stdout-line && echo stderr-line 1>&2"
# Python — only the second line will reach the model
python -c "import sys; print('stdout-line'); sys.stderr.write('stderr-line\n')"
# Node — only the second line will reach the model
node -e "console.log('stdout-line'); console.error('stderr-line');"
All four reproducers fail in the same way on this machine across every fresh session, regardless of model (gpt-5.4, gpt-5.5), reasoning effort, sandbox mode, or working directory.
Notes on session metadata
- Session ID: not session-specific; reproduces in every fresh thread on this install.
- Token usage / context window: N/A — this is a tool-layer bug, not a model behavior. Trivial single-turn prompts reproduce it.
- Conversation length: irrelevant; happens on first turn.
What is the expected behavior?
The shell_command tool result body delivered to the model should contain both the child process's stdout and stderr output, just like every other agentic CLI tool (Claude Code, Cursor agent, etc.) does on Windows.
For the minimal reproducer:
Write-Output 'stdout-line'
[Console]::Error.WriteLine('stderr-line')
the tool result body the model receives should be:
stdout-line
stderr-line
…rather than just:
stderr-line
Same expectation for every row in the matrix above where it currently shows ❌ — every one of those should be visible to the model, since the bytes do reach the IPC pipe (proven by them being rendered in the live TUI preview).
Additional information
Suspected root cause (source-level analysis)
Looking at the elevated sandbox path on main (commit 7d47056ea):
codex-rs/windows-sandbox-rs/src/process.rscorrectly creates separate anonymous pipes for stdout and stderr whenStderrMode::Separateis used:
``rust``
let stderr_handle = match stderr_mode {
StderrMode::MergeStdout => out_w,
StderrMode::Separate => err_w,
};
codex-rs/windows-sandbox-rs/src/bin/command_runner/win.rsspawns two symmetric reader threads, each tagging frames with the rightOutputStreamvariant:
``rust``
let out_thread = spawn_output_reader(
Arc::clone(&pipe_write),
stdout_handle,
OutputStream::Stdout, // stdout-tagged frames
log_dir_owned.clone(),
);
let err_thread = if stderr_handle != INVALID_HANDLE_VALUE {
Some(spawn_output_reader(
Arc::clone(&pipe_write),
stderr_handle,
OutputStream::Stderr, // stderr-tagged frames
log_dir_owned.clone(),
))
} else { None };
So the runner side is symmetric and definitely emits OutputStream::Stdout frames over the IPC pipe.
- The bug therefore has to be on the parent (
codex.exe) side — most likely incodex-rs/windows-sandbox-rs/src/elevated/runner_client.rsor whatever consumes the framedOutputpayloads. Hypothesis:
OutputStream::Stdoutframes are forwarded only to the live TUI preview.- When assembling the final
shell_commandtool_result body, onlyOutputStream::Stderrframes are concatenated.
This perfectly matches the user-visible symptom and explains why stdout is entirely missing rather than truncated/raced.
Workarounds (until fix lands)
| Runtime | Append/replace |
| --- | --- |
| PowerShell 7 | command *>&2 (redirects all 6 PS streams to stderr) |
| PowerShell (more reliable for native exes) | command *>&1 \| ForEach-Object { [Console]::Error.WriteLine($_) } |
| cmd.exe | command 1>&2 |
| Python | print(x, file=sys.stderr) |
| Node | console.error(x) instead of console.log(x) |
These all work today on the affected install.
Why this matters
This bug effectively breaks every non-trivial agentic flow on Windows Codex Desktop:
- File reads via
Get-Content→ "no output" → model thinks file is empty → corrupts edits - Listing directories via
Get-ChildItem→ model can't navigate the repo - Running tests where
pytest/cargo test/npm testprint to stdout → model thinks tests are silent / hung - Even
pwd/Get-Locationdoesn't tell the model where it is
Several existing issues hand-waved as "Windows is just buggy" almost certainly trace back here — e.g. #13234 ((no output) for everything on Windows). The pattern in this issue is much more specific: stderr always works, stdout never does.
Environment cleanup already verified (NOT the cause)
To preempt the usual "did you check X" replies, all of these have been independently verified on the same machine and ruled out:
- ✅
where.exe pwshresolves cleanly toC:\Program Files\PowerShell\7\pwsh.exe— nopwsh.cmdwrapper hijack (cf. #7741) - ✅ ExecutionPolicy:
LocalMachine = RemoteSigned, all other scopesUndefined—pwsh -NoProfile -Command "Get-Location"round-trips stdout perfectly when invoked manually outside Codex - ✅ Antivirus whitelisted; no recent script/exec blocks
- ✅
~/.codex/.sandbox/sandbox.logshows every recentpwsh.exeinvocation asSUCCESS:withexit code 0 - ✅ Live TUI preview does render stdout chunks momentarily — confirming stdout bytes really do flow from the child all the way to the IPC pipe; they just don't make it into the model's tool_result
- ✅ Tested after fully cleaning a 4 GB bloated
codex-tui.logand resetting~/.codex/.sandbox/setup_marker.json/setup_error.json. Bug persists, ruling out IO contention from log writes.
Notes for triage
- The
command_runnerside is genuinely symmetric (see source citations above), so a fix should not need to touch it. - Working hypothesis for fix location: search
codex-rs/windows-sandbox-rs/src/elevated/runner_client.rsfor the consumer ofOutputPayload { stream, .. }and verify it appends bothStdoutandStderrdata to whatever buffer eventually populates theshell_commandtool_result body. - Suggested regression test: spawn a child that writes a unique sentinel to stdout and a different sentinel to stderr; assert both appear in the tool_result string sent to the model (not just in the live preview frames).
Happy to run any additional diagnostics on this Windows machine if it helps.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗