[BUG] high token usage because of duplicated bash tool output
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
I think I found a hint on why Claude Code token usage is through the roof for me lately. When trying to optimise token usage of some helper scripts my eye caught the following in Claudes reasoning:
<img width="839" height="136" alt="Image" src="https://github.com/user-attachments/assets/df869116-1dd4-4c65-9f08-d412312392d1" />
So, I sent Claude to explore this, and the result is that the bash tool seem to duplicate all output Claude reads if the command exits with a non zero exit status. Means: test or build failure with each 100 lines? Well, Claude sees 200 lines. The multiplication per session is insane.
Claudes Report
Bug Report: Bash Tool Output Duplicated for Failed Commands
### Summary
When a Bash command exits with a non-zero status code, the output displayed in Claude Code's tool result
is duplicated (shown twice). Successful commands (exit 0) display output correctly once.
### Environment
- Claude Code CLI
- macOS Darwin 24.6.0
- Model: claude-opus-4-5
### Steps to Reproduce
Run this via the Bash tool:
echo "Line 1" && echo "Line 2" && exit 1
Tool result shows:
Line 1
Line 2
Line 1
Line 2
Expected:
Line 1
Line 2
### Verification That Actual Output Is Not Duplicated
(echo "Line 1" && echo "Line 2" && exit 1) 2>&1 | wc -l
# Returns: 2 (correct)
The shell produces 2 lines. The tool result displays 4.
### Comparison: Success vs Failure
| Command | Exit Code | Output Duplication |
| --- | --- | ---|
| echo "Line 1" && echo "Line 2" |0|No (shown once) |
| echo "Line 1" && echo "Line 2" && exit 1 |1| Yes (shown twice) |
### Impact on Context Usage
Yes, this increases context usage. The duplicated output consumes approximately 2x the tokens it should.
For verbose compiler errors or test failures, this can add thousands of unnecessary tokens to the
conversation context.
### Additional Observations
- The duplication appears in the <error> block of the tool result
- Both stdout and stderr content are duplicated together
- The duplication is identical (not interleaved or reordered)
- Wrapping in a subshell and counting lines confirms the actual output is correct
### Workaround
None identified. The duplication happens at the tool result display level, not in the actual command
execution.
---
What Should Happen?
No duplicate output
How to reproduce
- Open fresh Claude session
- Run
! echo "Line 1" && echo "Line 2" && exit 1
<img width="408" height="301" alt="Image" src="https://github.com/user-attachments/assets/f33bfa22-9bfb-4bdb-8b12-6bfbf996d4aa" />
Claude Model
Opus
Is this a regression?
I don't know
Claude Code Version
2.1.50
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
13 Comments
Confirmed on Claude Code 2.1.50 (npm), macOS 15.4, Darwin 25.3.0.
Successful commands show output once as expected.
Thanks!
Additional reproduction case — Windows 11 / Cowork session
Reporting as a comment to confirm this is the same bug, with additional environment details.
Environment
What we observed
When running any Python script via the Bash tool inside a Cowork session, all stdout output
is returned twice in the tool result. The script executes once — the duplication is in the
tool output returned to the agent, not in the script execution itself.
This was confirmed by:
Impact
Every Bash tool call with verbose output consumes approximately 2x the expected tokens.
This compounds significantly on long-running agentic tasks — in our case, a web scraping
pipeline where each script run produces multi-line status output.
Session architecture
Duplicate output appears to originate from stdout being captured at both the local agent
layer and the parent session layer. Session files are stored at:
AppData/Roaming/Claude/local-agent-mode-sessions/<session-id>/Workaround
Redirecting all status/progress
print()calls tostderrinstead ofstdoutpreventsthe duplication, since only
stdoutappears to be echoed twice. This is the Unix-standardpattern for separating status messages from data output and works correctly as a mitigation.
Relation to existing report
This appears to be the same root cause as the existing report. Submitting as an additional
data point confirming the bug on Windows 11 with a Cowork session context, which
may differ from the original reporter's environment.
Bump to avoid auto-close.
It's not just a Mac issue. I'm using Windows 11.
The problem appears to be that it merges stdout and stderr to a single stream and then sends the _merged_ stream to both stdout and stderr. You can see this in the following screenshot.
In the screenshot,
<img width="818" height="214" alt="Image" src="https://github.com/user-attachments/assets/14237011-0fec-4f31-835f-68e8d3895730" />
To be clear, this doesn't just happen with shell overrides. It also happens whenever Claude uses a bash tool that has a nonzero return value:
<img width="1465" height="656" alt="Image" src="https://github.com/user-attachments/assets/e320d6f6-f2bb-42cd-9366-1db1f2e53e51" />
(Windows 11 Pro, version 10.0.26200 Build 26200. Also using, Git Bash, Opus 4.6, and Claude Code CLI 2.1.56.)
Tried to write a
PostToolUseFailurehook to dedup. But it appears as if hooks can only extend tool output, not replace it. Anyone had luck working around the issue here?This bug should be prioritised because it contributes to burn tokens unnecessarily!
I asked Sonnet 4.6 to investigate. Not only it found the root cause but also came up with a patch the fixes it. In its own words:
"""
Root cause (found by tracing cli.js)
The duplication is in the bash tool's error path. When a command exits non-zero:
annotateStderrWithSandboxFailuresreturns its input unchanged when there are no sandbox violations (the normal case), soShellError.stderr === ShellError.stdout === X.stdout. When the error is caught, the tool result is assembled as:The fix is trivial: pass
""instead ofX.stdout. The bash tool doesn't capture stderr separately anyway (it's merged into stdout already), so there's nothing to annotate.Self-patch for v2.1.69 (one occurrence, safe to apply)
Find your
cli.jsfirst:Verify the pattern exists (should print
1):Apply (creates a
.backupfirst):Verify the fix landed:
Restart Claude Code. Note this patch applies to the current claude version 2.1.69. Variable names (
A,X) may differ in future versions, butannotateStderrWithSandboxFailuresis unique in the file so it's easy to re-locate."""
When I asked to corroborate the claim that "The bash tool doesn't capture stderr separately anyway (it's merged into stdout already)", Sonnet replied with this:
"""
The bash tool spawns the shell with both stdout and stderr pointing to the same file descriptor N:
Nis an append-mode file descriptor (fs.openSyncwithO_APPEND). Because both streams share the samefd,A.stderron the child process object isnull— there is no separate stderr pipe.Consequence:
taskOutput.getStdout()returns everything (stdout + stderr combined), andtaskOutput.getStderr()always returns""."""
I merged a fix for it today and it'll go out in tomorrow's release.
thanks!
oops, github auto-closed this but it'll be in tomorow's release. :)
Fixed in 2.1.71. Thanks! 🙏
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.