[BUG] Bash tool returns empty output for SSH commands that work in regular terminal
Preflight Checklist
- [x] Searched existing issues
- [x] Single bug report
- [x] Using latest version
What's Wrong?
The Bash tool silently swallows all stdout from SSH remote commands in certain cases. The exact same commands produce full output when run in a regular terminal.
A compound SSH test command returns complete output in Terminal.app but returns (No content) / empty through the Bash tool:
echo "=== TEST 1: qm list ===" && ssh root@<remote-host> 'qm list' && echo "=== TEST 2: pct list ===" && ssh root@<remote-host> 'pct list' && echo "=== TEST 3: echo PATH ===" && ssh root@<remote-host> 'echo $PATH' && echo "=== ALL TESTS DONE ==="
- Terminal.app result: All test headers, SSH command output, and markers displayed correctly
- Bash tool result: Completely empty — no output at all
Even simple commands like ssh root@<remote-host> 'qm list' return empty. The remote commands do execute — verified by redirecting to a file on the remote host and reading it in a separate SSH session.
Notably, some SSH commands work fine through the Bash tool (e.g., ssh root@<remote-host> 'hostname && uptime && free -h'), while others consistently return empty (e.g., ssh root@<remote-host> 'qm list', ssh root@<remote-host> 'echo $PATH'). The affected commands produce clean ASCII output with no special characters (verified via hex dump).
What Should Happen?
The Bash tool should return the same stdout/stderr as running the command directly in a terminal.
Steps to Reproduce
- Have an SSH-accessible remote host running Proxmox VE
- Verify
ssh root@<remote-host> 'qm list'works in a regular terminal - Run the same command through Claude Code's Bash tool
- Observe empty output
Minimal reproducer (any command that produces column-padded output over SSH may trigger this):
ssh root@<remote-host> 'qm list'
Additional Findings (2026-02-18)
Further investigation revealed several new data points:
Not limited to SSH — gh CLI also affected
The gh issue view command exhibits the same empty output behavior, suggesting this is a broader subprocess stdout capture problem, not SSH-specific.
Local file redirect workaround works
In addition to the remote-side file redirect, redirecting locally also recovers the output:
ssh root@<remote-host> 'qm list' > /tmp/output.txt 2>&1
# Then read /tmp/output.txt — contains the full expected output
Exit code is 0 and the file contains correct output, confirming the command executes successfully.
TCP connectivity is fine
nc -z -w 3 <remote-host> 22 returns success and captures output correctly through the Bash tool. Only the actual SSH session output is lost.
First invocation in a session can succeed
On the very first Bash tool invocation in a fresh session, SSH output was captured correctly. Every subsequent invocation returned empty. This suggests some state change within the sandbox/tool after the first execution.
VPN split tunnel is not a factor
Initially suspected VPN routing, but the issue persists with VPN disconnected. The routing table correctly sends 192.168.8.0/24 traffic through the local interface regardless of VPN state.
Workarounds
- Local file redirect:
ssh root@<host> 'command' > /tmp/out.txt 2>&1then read the file - Remote file redirect:
ssh root@<host> 'command > /tmp/out.txt' && ssh root@<host> 'cat /tmp/out.txt' - Run directly in terminal and paste results back to Claude Code
Is this a regression?
I don't know
Claude Code Version
2.1.45
Platform
Anthropic API
Operating System
macOS 26.2 (arm64)
Terminal/Shell
Terminal.app / zsh inside tmux
Claude Model
Opus 4.6
Related Issues
- #19663 — Bash tool returns
(No content)for all commands on macOS - #22627 — CLI freezes when reading file content over SSH pipes
- #16431 — Commands with pipes or
cd && ...patterns run in background with 0-byte output - #21143 — Bash command failing with no output
18 Comments
Meta note: even the
gh issue createandgh issue listcommands used to file this bug had their output swallowed by the Bash tool. Had to use the same file-redirect workaround (gh issue list --json ... > /tmp/out.json && cat /tmp/out.json) just to confirm the issue was created.The bug is eating its own bug report. 🐛
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Related Issues — Common Root Cause
After investigating, this appears to be part of a systemic problem with the Bash tool's stdout capture mechanism. Three related issues exhibit overlapping symptoms:
(No content)for all commands on macOS. Root cause traced to shell snapshots containingset -o onecmd, breaking the multi-step wrapper script. Affects all platforms on v2.1.x.stat,ls) work; content commands (tail,cat) freeze. Points to broken streamed SSH output handling.|) orcd && ...patterns silently get routed to background execution with 0-byte output files. Labeledhas repro,platform:macos.How this issue adds to the picture
This issue provides additional diagnostic evidence:
ssh host 'hostname && uptime'works whilessh host 'qm list'returns empty — suggesting the issue is sensitive to the remote command's output buffering behavior (Perl-based tools like Proxmoxqm/pctvs. standard coreutils)ghCLI output is swallowed — this isn't SSH-specific; thegh issue createandgh issue listcommands used to file this very bug also returned emptyCommon thread
All four issues point to broken subprocess stdout capture in the Bash tool, triggered by some combination of:
Bash commands keep returning "Error: Exit code 1" after the standalone Claude installation.
`
● Bash(pwd 2>&1)
⎿ Error: Exit code 1
● Bash(/bin/pwd 2>&1; echo "EXIT: $?")
⎿ Error: Exit code 1
● Bash(echo "hello world")
⎿ Error: Exit code 1
● Bash(whoami)
⎿ Error: Exit code 1
● Bash(echo "hello") ⎿ Error: Exit code 1 `
Only this works:
● Bash(true)⎿ (No output)
I'm on windows, Claude Code version is 2.1.45.
Bash works fine outside of Claude Code.
Exact same issue popped up here today after working fine for weeks, months...
Nothing special there.
Having the same issue.
I uninstalled and reinstalled Claude. Tried the native installation and also through Homebrew.
Facing this on v2.1.49 and been an issue since v2.1.30. I'm on MacOS 15.7.3 on MBP M1Pro
Update: Root Cause Narrowed — Internal Pipe Reader Timeout (~270ms)
Universal Local Reproducer (no SSH, no dependencies)
This proves the bug is not SSH-related. Any external binary that delays stdout by ~270ms+ triggers it.
Findings
1. Precise timeout threshold: ~270-280ms
Tested locally with
/bin/sleep(external binary):| Delay | Output captured? |
|-------|-----------------|
| 200ms | ✅ Yes |
| 250ms | ✅ Yes |
| 270ms | ✅ Yes |
| 280ms | ❌ No |
| 290ms | ❌ No |
| 300ms | ❌ No |
The earlier SSH threshold appeared to be ~380ms because SSH adds ~100-120ms of connection overhead before the remote command starts.
2. Only affects external binaries, not shell builtins
This suggests the pipe reader closes after the forked child process exits but before the parent shell flushes its remaining stdout (the
echo).3. Python reproducer (also works)
Same threshold. Any subprocess that takes >270ms before writing to stdout gets silently dropped.
4. Not caused by hooks or config
Disabled the PreToolUse hook entirely — bug persists. The issue is internal to the Bash tool's subprocess output capture.
5. Intermittent "first run works" explained
The first Bash tool invocation in a fresh session sometimes captures output correctly. Subsequent invocations fail. This is consistent with a pipe reader or event loop listener that initializes correctly but doesn't properly reset its state between invocations.
6. Not macOS version specific
Confirmed by @ugattupalli-coatue on macOS 15.7.3 (Sequoia) and by us on macOS 26.2 (Tahoe). Also reported on Windows. This is a cross-platform Claude Code bug.
Likely Mechanism
The Bash tool's Node.js subprocess handler appears to have a race condition in its stdout pipe reader. When an external binary is forked (fork+exec), the pipe reader seems to interpret the child process exit as "done reading" and closes the pipe — but the parent shell still has pending output to flush. Commands that complete within ~270ms finish before this premature close; anything slower loses its output.
Environment
Update 2: Debug Log Timing Analysis — Shell Exit Before Stdout Drain
Ran Claude Code with
--debug-file /tmp/claude-debug.logand executed/bin/sleep 0.3 && echo "hello"twice. First run captured output, second run swallowed it.Debug Log: First Run (OUTPUT CAPTURED ✅)
Total duration: ~1,288ms
The snapshot creation adds ~960ms of overhead. This gives
/bin/sleep 0.3 && echo "hello"plenty of time — the sleep and echo finish well before the tool closes the pipe.Debug Log: Second Run (OUTPUT SWALLOWED ❌)
Total duration: ~332ms
No snapshot creation on the second run — the shell is reused. The tool declares "done" at 332ms, which is almost exactly when
/bin/sleep 0.3finishes. Theecho "hello"hasn't flushed to the pipe yet when the tool closes it.Root Cause
The Bash tool's "command complete" signal is based on the shell process exiting, not on stdout being fully drained.
echo) flushes.No Errors Logged
The debug log contains zero errors or warnings related to stdout/pipe handling. The tool silently believes the command produced no output. There is no EPIPE, no broken pipe, no drain warning — the race condition is completely invisible to the current logging.
The Fix
The tool needs to wait for the stdout pipe EOF (i.e., fully drain the readable stream) before declaring the command complete, rather than relying on process exit timing alone. This is a classic Node.js subprocess gotcha —
child.on('exit')fires beforechild.stdout.on('end')when there's buffered data.The standard pattern is:
Environment
--debug-file /tmp/claude-debug.logUpdate: Regression identified — introduced in v2.1.45
We bisected this issue and confirmed it's a regression starting at v2.1.45.
Test methodology
Repeated runs of
/bin/sleep 0.3 && echo "hello"across versions:| Version | Result |
|---------|--------|
| 2.1.44 | Consistent output — 3/3 passes at 0.3s, also passes at 0.5s |
| 2.1.45 – 2.1.56 | Intermittent silent stdout loss at ≥0.3s delay |
Reproduction on 2.1.56
During a single session, the same command alternated between returning
helloand returning empty output (Tool ran without output or errors), confirming the race condition is timing-dependent.Likely culprits in 2.1.45
The 2.1.45 changelog includes changes to startup performance, memory usage, and Task tool process handling. Subsequent versions made further BashTool changes:
-lflag) by default when a shell snapshot is available"The Windows stdout fix in 2.1.47 suggests the team is aware of pipe handling issues in the BashTool but the macOS/Linux variant remains unfixed.
Environment
Requesting maintainer review — regression bisected, root cause identified, local reproducer provided
This issue has a universal local reproducer (no SSH, no dependencies):
Root cause: The BashTool's pipe reader closes on process exit rather than waiting for stdout to fully drain. The threshold is ~270ms — any external binary that delays stdout beyond that gets silently dropped. This is the classic Node.js
child.on('exit')vschild.on('close')race condition.Regression bisected to v2.1.45 — v2.1.44 works consistently. The same class of bug was already fixed for Windows in v2.1.47 ("bash tool output silently discarded on Windows with MSYS2/Cygwin"), but the macOS/Linux variant remains.
This also unifies at least 4 open issues with zero maintainer responses: #19663, #22627, #16431, #21143.
The fix is a one-liner — wait for stream drain before resolving:
@whyuan-cc @ddworken @claude — would any of you be able to take a look? This affects every platform on v2.1.45+ and has multiple community reports.
Update: Root Cause Identified —
claude mcp listin Custom StatuslineAfter extensive investigation, we identified the actual root cause on our end: a custom statusline command that called
claude mcp liston every refresh cycle.The Setup
Our
settings.jsonincluded a custom statusline:The script displayed model, cost, context usage, git branch, MCP server status, and AWS SSO token TTL:
The culprit is
claude mcp list— it spawns an entire Claude Code CLI process (full Node.js application startup) on every statusline refresh. Compare that to the other commands in the script (jq,git rev-parse,sed,grep) which are lightweight binaries that return in microseconds. Running a full Node.js app on a periodic loop saturates the event loop and causes the Bash tool's stdout pipe reader to lose the race.How We Confirmed It
| Configuration |
/bin/sleep 2.0 && echo "hello"||---|---|
| Statusline with
claude mcp list| Fails at ~270-320ms (threshold varies with system load) || Statusline without
claude mcp list| Passes at 2000ms+ || Statusline completely disabled | Passes at 2000ms+ |
Other factors we ruled out:
.zshrc(NVM 895ms, direnv 262ms, SDKMAN 40ms) — shifted threshold by ~10-20ms at mostCorrection to Previous Analysis
Our earlier comments identified this as a Bash tool pipe handling bug (using
child.on('exit')instead ofchild.on('close')). That analysis was wrong — or at minimum, incomplete. The ~270ms threshold we measured was not an inherent timeout in the Bash tool's pipe reader. It was the result ofclaude mcp listsaturating the event loop on our specific machine.We cannot confirm whether the underlying race condition exists independently of this statusline issue. On our system, with
claude mcp listremoved from the statusline, we see zero stdout loss even at multi-second delays.Recommendation
If you're experiencing this issue, check whether you have a custom statusline or hooks that spawn heavyweight processes (especially
claude mcp listor other CLI subprocesses). Disabling or simplifying those may resolve it.A lighter-weight way to query MCP server status from within a statusline would be useful —
claude mcp listis too expensive to call on a periodic refresh loop.Environment
For slow commands > 0.3seconds, can someone confirm if they can write the command to the file?
eg. Bash(slow command) gives no output for me, but Bash(slow command) > foo.txt saves the output to the file
@AllenThomasDev Yes, the file redirect workaround works. Both of these approaches capture the output correctly:
That said, check our latest update above — the root cause on our end turned out to be a custom statusline running
claude mcp liston every refresh cycle, which saturated the event loop. After removing that, we no longer see stdout loss even at multi-second delays. If you have any custom statuslines or hooks spawning heavyweight processes, disabling those may fix it entirely.+1 — also hitting this on macOS (arm64) with Claude Code 2.1.63. In my case, the Bash tool loses all stdout when the command launches a nested Claude Code subprocess via the claude-agent-sdk
query()function. Output is written correctly (verified via file redirect andos.write(1)returning success), but the Bash tool captures nothing. File redirect workaround confirms.Hadn't made the connection before but now that you mention it, that is exactly the same problem I've been having. I've been seeing the same thing as @possibilities -- when the Bash tool spawns a subprocess that internally runs Claude Code, the parent Claude sees nothing on the Bash output.
My subprocess is running
claudewith streaming JSON input/output instead of using agent SDK but basically the same thing. I also worked around for now with solutions such as file redirect.Maybe it wasn't a useful exercise but I tried moving the invocation out to a separate script that gets shelled out to and still nothing. That was just me flailing but thought I'd mention it.
Closing for now — inactive for too long. Please open a new issue if this is still relevant.
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.