[BUG] high token usage because of duplicated bash tool output

Status Fixed / completed
Reported on v2.1.50
Maintainer reply ✓ Yes — amorriscode
Activity 13 comments · opened Feb 22, 2026 · closed Mar 6, 2026
💡 Likely answer: A maintainer (amorriscode, contributor) responded on this thread — see the highlighted reply below.

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

  1. Open fresh Claude session
  2. 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)

View original on GitHub ↗

13 Comments

vasylNaumenko · 6 months ago

Confirmed on Claude Code 2.1.50 (npm), macOS 15.4, Darwin 25.3.0.

$ echo "Line 1" && echo "Line 2" && exit 1

# Tool result:
Line 1
Line 2

Line 1
Line 2

Successful commands show output once as expected.

NickSdot · 6 months ago
Confirmed on Claude Code 2.1.50 (npm), macOS 15.4, Darwin 25.3.0.

Thanks!

inner-success · 6 months ago

Additional reproduction case — Windows 11 / Cowork session

Reporting as a comment to confirm this is the same bug, with additional environment details.

Environment

  • OS: Windows 11 Home (Build 10.0.26200)
  • Python: 3.14.3 (MSC v.1944 64-bit AMD64)
  • Session type: Claude Code Cowork (local agent mode session)

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:

  • Output files being written correctly (once only)
  • All print statements appearing exactly twice in the tool result
  • The issue not occurring outside of Cowork/local agent mode sessions

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 to stderr instead of stdout prevents
the duplication, since only stdout appears to be echoed twice. This is the Unix-standard
pattern 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.

NickSdot · 6 months ago

Bump to avoid auto-close.

robmunger · 6 months ago

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,

  • White output represents stdout (but contains the merged stream)
  • Red output represents stderr (but contains the merged stream)

<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.)

NickSdot · 5 months ago

Tried to write a PostToolUseFailure hook to dedup. But it appears as if hooks can only extend tool output, not replace it. Anyone had luck working around the issue here?

anthrotype · 5 months ago

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:

// The combined output buffer (X.stdout) is passed as the "stderr" argument by mistake
let u = xA.annotateStderrWithSandboxFailures(A.command, X.stdout || "");
throw new ShellError(X.stdout, u, X.code, X.interrupted);
//                   ^stdout   ^stderr — both are X.stdout

annotateStderrWithSandboxFailures returns its input unchanged when there are no sandbox violations (the normal case), so ShellError.stderr === ShellError.stdout === X.stdout. When the error is caught, the tool result is assembled as:

["Exit code N", "", shellError.stderr, shellError.stdout]
// = ["Exit code N", "", X.stdout,     X.stdout]   ← output twice

The fix is trivial: pass "" instead of X.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.js first:

# common paths:
# /usr/lib/node_modules/@anthropic-ai/claude-code/cli.js
# ~/.claude/local/node_modules/@anthropic-ai/claude-code/cli.js
# $(npm root -g)/@anthropic-ai/claude-code/cli.js

CLI=$(npm root -g)/@anthropic-ai/claude-code/cli.js

Verify the pattern exists (should print 1):

grep -c 'annotateStderrWithSandboxFailures(A.command,X.stdout||"")' "$CLI"

Apply (creates a .backup first):

cp "$CLI" "$CLI.backup"
sed -i 's/annotateStderrWithSandboxFailures(A\.command,X\.stdout||"")/annotateStderrWithSandboxFailures(A.command,"")/g' "$CLI"

Verify the fix landed:

grep -c 'annotateStderrWithSandboxFailures(A.command,"")' "$CLI"  # should print 1

Restart Claude Code. Note this patch applies to the current claude version 2.1.69. Variable names (A, X) may differ in future versions, but annotateStderrWithSandboxFailures is unique in the file so it's easy to re-locate.
"""

anthrotype · 5 months ago

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:

// cli.js — bash tool spawn
let L = sn9(W, G, {
  cwd: P,
  stdio: ["pipe", N, N],  // ← stdout and stderr both go to N (same fd)
  windowsHide: !0
});

N is an append-mode file descriptor (fs.openSync with O_APPEND). Because both streams share the same fd, A.stderr on the child process object is null — there is no separate stderr pipe.

Consequence: taskOutput.getStdout() returns everything (stdout + stderr combined), and taskOutput.getStderr() always returns "".
"""

amorriscode contributor · 5 months ago

I merged a fix for it today and it'll go out in tomorrow's release.

anthrotype · 5 months ago

thanks!

amorriscode contributor · 5 months ago

oops, github auto-closed this but it'll be in tomorow's release. :)

NickSdot · 5 months ago
I merged a fix for it today and it'll go out in tomorrow's release.

Fixed in 2.1.71. Thanks! 🙏

github-actions[bot] · 5 months ago

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.