[BUG] Environment Variables Cleared When Using Pipe Operator in Bash Tool
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?
When using the Bash tool to access a custom environment variable, say CLAUDE_AGENT_ID, with a pipe operator, the variable expands to only a newline character instead of its actual value "alpha". The environment variable exists and is visible with env | grep CLAUDE_AGENT_ID, but when piped to another command like wc -c, it loses its value.
What Should Happen?
The CLAUDE_AGENT_ID environment variable should consistently expand to its actual value "alpha" when used in any bash command, including when piped to other commands.
Error Messages/Logs
# Variable exists in environment
$ env | grep CLAUDE_AGENT_ID
CLAUDE_AGENT_ID=alpha
# But expands to only newline when piped
$ echo "$CLAUDE_AGENT_ID" | hexdump -C
00000000 0a |.|
00000001
# Character count shows 1 instead of expected 6
$ echo $CLAUDE_AGENT_ID | wc -c
1
Steps to Reproduce
Please provide clear, numbered steps that anyone can follow to reproduce the issue. Important: Include any necessary code, file contents, or context needed to reproduce the bug. If the issue involves specific files or code, please create a minimal example.
- In Claude Code, use the Bash tool to echo the CLAUDE_AGENT_ID:
``bash``
echo $CLAUDE_AGENT_ID
Result: Shows "alpha" (appears correct)
- Use the Bash tool to count characters with a pipe:
``bash`
echo $CLAUDE_AGENT_ID | wc -c
1` (incorrect - should be 6)
Result: Returns
- Verify the variable exists:
``bash`
env | grep CLAUDE_AGENT_ID
CLAUDE_AGENT_ID=alpha` (correct)
Result: Shows
- Check actual bytes being output:
``bash`
echo "$CLAUDE_AGENT_ID" | hexdump -C
0a` (newline character)
Result: Shows only
- Run in a new bash subprocess:
``bash`
bash -c 'echo "$CLAUDE_AGENT_ID" | wc -c'
6` (correct - works in subprocess)
Result: Returns
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
1.0.128 (Claude Code)
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
WSL (Windows Subsystem for Linux)
Additional Information
Anything else that might help us understand the issue?
- The bug only occurs when using pipes with the environment variable
- Direct echo without pipes appears to work (shows "alpha")
- The variable works correctly when executed in a new bash subprocess using
bash -c - This appears to be a shell state issue where the variable exists in the environment but isn't properly accessible in the current shell context
- Use case where discovered: I observed this issue when trying to inject basic authentication credentials for curl commands using environment variables (e.g.,
curl -u "$USERNAME:$PASSWORD" https://api.example.com). The authentication would fail because the variables weren't expanding correctly when piped or used in command substitution. - Date observed: 2025-09-28
16 Comments
Additional Context: Affects More Than Just Pipes
I can confirm this bug and wanted to add that the issue extends beyond just pipe operators - environment variables are also empty when passed to subprocess commands like
curl.Real-World Impact Example
When trying to use an API token in a curl Authorization header:
Result: Authentication fails because curl receives an empty token, even though:
${#MY_API_TOKEN}correctly returns 501 (the token length)env | grep MY_API_TOKENshows the full token valueInconsistency Details
Confirmed Workaround
Writing commands to a script file and executing them works correctly:
This suggests the issue is specifically with how the Bash tool passes environment variables to inline commands vs. script files.
Verified: (the CLAUDE_AGENT_ID doesn't seem to exist anymore, but the same happens with CLAUDE_CODE_ENTRYPOINT)
Workaround: advise Claude to always use bash <<'BASH' .... BASH when using the Bash tool.
PreToolUse Hook Workaround for Bash Preprocessing Bugs (v7)
A hook that fixes all known bash preprocessing bugs in Claude Code by wrapping commands in
bash -c '...'.Zero token overhead — hooks run externally before command execution.
---
v7 Update: "Wrap Everything" Approach
Credit: @AdamScherlis identified a gap in v6's pattern-matching. Commands like
echo $CLAUDE_CODE_ENTRYPOINT | wc -cweren't being wrapped because they don't match any detection patterns (no$(), no newlines, no loop keywords) — yet they still trigger the preprocessing bug.Why Pattern-Matching Failed
v1-v6 tried to detect "problematic" commands by looking for:
$(...)command substitutionfor ... | ...)This was ~30 lines of regex that inevitably had blind spots.
The Fix: Wrap Everything
AdamScherlis's suggestion: just wrap ALL commands unconditionally.
Concerns we evaluated:
| Concern | Finding |
|:--------|:--------|
| Performance overhead? | Negligible —
bash -cadds microseconds || Breaks existing commands? | Tested 10,749 real Claude commands — no issues |
| Escape sequence differences? |
/bin/shvsbashdiffer on some escapes, but Claude usesecho -eor$'...'when it needs escape interpretation — never relies on raw\tin double quotes |Result: Simpler code, more robust, future-proof against unknown bugs.
---
GitHub Issues Fixed
| Issue | Problem |
|:------|:--------|
| #11225 |
$(...)command substitution mangled || #11182 | Multi-line commands have newlines stripped |
| #8318 | Loop variables silently cleared with pipes |
| #8318 | Environment variables cleared with pipes (AdamScherlis) |
| #10014 | For-loop variable expansion issues |
---
Installation
Step 1: Save the hook
Step 2: Make executable
Step 3: Configure Claude Code
Run
/hooksin Claude Code, then:~/.claude/hooks/fix-bash-substitution.pyStep 4: Restart session
Start a new Claude Code session for the hook to take effect.
---
Verifying It Works
---
Escape Markers
To skip wrapping for a specific command, add a comment:
---
Changelog
| Version | Date | Changes |
|:--------|:-----|:--------|
| v7 | 2025-12-25 | "Wrap everything" — @AdamScherlis's approach. Simpler, more robust. |
| v6 | 2025-12-11 | Control structure detection for nested if/for/while |
| v5 | 2025-12-10 | Skip continuation-fixing for control structures |
| v4 | 2025-12-10 | Heredoc detection |
| v3 | 2025-12-10 | Quote-aware continuation fixing |
| v2 | 2025-12-09 | Loop-with-pipe detection |
| v1 | 2025-12-09 | Initial release |
The hook above does not fix this issue, because it doesn't include the "env variable + pipe" failure mode in
needs_wrapping().e.g.
echo $CLAUDE_CODE_ENTRYPOINT | wc -cwill not get wrapped.Skipping the
needs_wrappingcheck and wrapping every bash call seems to work fine; are there downsides to this approach?@AdamScherlis You're absolutely right — updated the hook to v7 with your "wrap everything" approach. Thank you for the catch and the elegant solution.
The Problem You Identified
echo $CLAUDE_CODE_ENTRYPOINT | wc -cwasn't getting wrapped because v6 looked for specific patterns:$(...)— not presentYet it still triggers the preprocessing bug. Pattern-matching will always have blind spots.
Evaluating "Wrap Everything"
Before adopting your approach, I tested potential downsides:
| Concern | Investigation | Result |
|:--------|:--------------|:-------|
| Performance? |
bash -coverhead | Microseconds — negligible || Breaks commands? | Tested 10,749 real Claude commands from history | Zero issues |
| Escape sequences? |
/bin/shinterprets\tdifferently thanbash -c| Claude usesecho -eor$'...'for escapes — never relies on the affected behavior |The only "failures" in the test suite were tests with wrong expectations (relying on
/bin/shquirks that differ from bash).The Fix
~30 lines of pattern detection → 5 lines. Simpler, more robust, future-proof.
Credit added to the hook source and main comment. Thanks again!
This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.
this is still happening
I just reported #29298, which does appear to be a duplicate of this bug. However, this bug report seems like it's being ignored a bit, maybe? So I don't want to close mine as a duplicate yet. It contains a bunch of test cases which may be helpful.
Also, I get the feeling this bug was fixed at some earlier versions and then reintroduced? Either that or I'm delusional.
Still an issue on 2.1.63. On mac
Just ran into this on macos 26.3, claude code 2.1.71, zsh 5.9 (arm64-apple-darwin25.0). It was very time-consuming for me to troubleshoot why the commands that worked in my terminal didn't work for claude; hope this gets fixed at some point!
A simple workaround that seems to be working for me is to tell claude to use
$(printenv VAR)instead of$VAR.There are workarounds for this - but we're spending quite a bit of time implementing them and deploying them widely in our org. It'd really save us a lot of time and pain if a proper fix could be implemented
This appears to have been fixed within the last couple of weeks
Indeed!
windows:
linux
This issue was fixed as of version 2.1.79.
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.