[BUG] Bash tool drops pipe stdin when command contains command substitution syntax (backticks or $(...))

Resolved 💬 3 comments Opened Feb 11, 2026 by danr Closed Mar 12, 2026

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?

The Bash tool silently drops pipe stdin when the command string contains command substitution syntax — either backticks (cmd) or $(cmd).
The left side of the pipe produces no output, so the right side receives empty stdin.

This affects real-world usage when piping content containing $(...) or backticks to commands. For example, piping a git commit message with heredoc syntax through a script:

echo 'git commit -m "$(cat <<'"'"'EOF'"'"'
commit message here
EOF
)"' | some_script bash

The eval '...' < /dev/null wrapper that the Bash tool uses appears to expand command substitution syntax inside the command string before the pipe runs, causing the < /dev/null to feed into the substituted command instead of being inert.

Pipe stdin should be preserved regardless of what characters appear in the command string. Single-quoted content should never be subject to command substitution expansion.

Bash(echo 'A$(B)C' | cat)
   (No content)

What Should Happen?

Pipe stdin should be preserved regardless of what characters appear in the command string. Single-quoted content should never be subject to command substitution expansion.

Bash(echo 'A$(B)C' | cat)   # should output A$(B)C.

Error Messages/Logs

No error messages. The failure is silent — the tool returns empty output with exit code 0.

Steps to Reproduce

  1. Create a test script that dumps its stdin (/tmp/test_stdin.py):
#!/usr/bin/env python3
import sys, json
raw = sys.stdin.read() if not sys.stdin.isatty() else None
print(json.dumps({"stdin": raw, "stdin_bytes": len(raw) if raw else 0}))
  1. Ask Claude Code to run these Bash commands:
# FAILS — empty output, stdin lost:
echo 'A$(B)C' | cat
echo 'A`B`C' | cat
echo 'commit -m "$(cat <<EOF
msg
EOF
)"' | python3 /tmp/test_stdin.py bash
# WORKS — adding ; true makes it work (different eval code path?):
echo 'A$(B)C' | cat; true
echo 'A`B`C' | cat; true
  1. Observe that commands with command substitution syntax produce empty output, but the identical commands with ; true appended work

correctly.

Systematic test results from within a Claude Code session:

┌───────────────────────────┬───────────┐
│          Command          │  Output   │
├───────────────────────────┼───────────┤
│ echo 'hello' | cat        │ hello ✓   │
├───────────────────────────┼───────────┤
│ echo 'A&&B|C' | cat       │ A&&B|C ✓  │
├───────────────────────────┼───────────┤
│ echo 'A$B' | cat          │ A$B ✓     │
├───────────────────────────┼───────────┤
│ echo 'A${B}C' | cat       │ A${B}C ✓  │
├───────────────────────────┼───────────┤
│ echo 'A\B`C' | cat`       │ (empty) ✗ │
├───────────────────────────┼───────────┤
│ echo 'A$(B)C' | cat       │ (empty) ✗ │
├───────────────────────────┼───────────┤
│ echo 'A$(' | cat          │ (empty) ✗ │
├───────────────────────────┼───────────┤
│ echo 'A\B`C' | cat; true` │ A\B`C` ✓  │
├───────────────────────────┼───────────┤
│ echo 'A$(B)C' | cat; true │ A$(B)C ✓  │
└───────────────────────────┴───────────┘

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.32

Platform

Anthropic API

Operating System

Other Linux

Terminal/Shell

Other

Additional Information

Related issues sharing the same eval '...' < /dev/null root cause:

  • #1132 — < /dev/null injected into middle of piped jq commands
  • #2383 — zsh pipes break with nomultios due to trailing < /dev/null
  • #2851 — < character causes /dev/null injection
  • #7387 — $(...) gets mangled to \$ ( ... ) with /dev/null injection
  • #15599 — multi-line + pipe commands with $() corrupted

The common root cause across all these issues is the eval '...' < /dev/null wrapper. The ; true workaround suggesting there are two code paths — the simpler one (no ;) handles the eval wrapping differently and breaks on command substitution syntax.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗