Skip to content

[BUG] Bash Tool Silently Empties Environment Variables in Pipelines #29298

@mhelvens

Description

@mhelvens

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • I am using the latest version of Claude Code

What's Wrong?

Bug report and tests written by Claude Code, but double-checked by me.

Summary

The Claude Code Bash tool silently expands all environment variables to empty strings when the command is a simple pipeline and does not have a # comment on a preceding line. This produces incorrect results with no warning, and the workaround (adding a comment) is non-obvious.

Impact

This is a silent correctness bug — commands produce wrong results with no error or warning. It is particularly insidious because:

  • It's intermittent in practice. Developers (and AI agents) habitually add # descriptive comment lines before commands. When they do, the bug doesn't trigger. When they don't, it does. This makes it hard to diagnose.
  • The failure mode is misleading. When a variable is used in a file path, the error looks like a path problem (bash: /scripts/setup.sh: No such file or directory), not an env var expansion issue. Agents waste time investigating the wrong cause.
  • It was discovered by accident. A Claude Code subagent failed to run a script, retried 3 times, then worked around it by hardcoding the path — all because it happened not to include a # comment in its first few attempts.

Analysis

  1. The variables exist in the process environment. env | grep VAR always shows the correct value. printenv VAR | cat also works. The variables are exported and present.

  2. Shell $-expansion is broken, not the environment. The issue is specifically that $VAR syntax expands to an empty string. Commands that read from the environment directly (env, printenv) are unaffected.

  3. The issue only manifests in pipelines. The same $VAR expansion works correctly when there is no | in the command.

  4. A # comment on a preceding line is a reliable workaround. This suggests the Bash tool has two code paths for executing commands, selected based on whether a # character appears in the input. The non-# path has a bug where environment variables are not available during shell expansion of pipelines.

  5. Compound commands (for, if/then) also work. These change how the shell parses the command, which may route through a different execution path.

  6. The set -x trace confirms eval is used. Trace output shows (eval):1> prefixes. The bug likely lies in how the command string is passed to eval or in environment setup before eval runs, conditional on the presence of #.

What Should Happen?

Environment variables should expand identically regardless of whether the command contains a pipe, and regardless of whether a # comment precedes the command.

Error Messages/Logs

Steps to Reproduce

Just ask Claude Code to run the following bash commands.

Failing cases — env vars silently expand to empty

Test 1: Simple pipe — $HOME expands to empty, so echo prints a blank line, and cat outputs nothing.

echo $HOME | cat

Output: (empty)


Test 2: Quoting doesn't help.

echo "$HOME" | cat

Output: (empty)


Test 3: Brace syntax doesn't help.

echo ${HOME} | cat

Output: (empty)


Test 4: It's not just echo and cat. When the variable is in a path argument, the empty expansion produces a bogus path.

bash "$HOME/some/script.sh" | head -1

Output (stderr): bash: /some/script.sh: No such file or directory


Test 5: All variables are affected, not just $HOME.

echo "HOME=$HOME USER=$USER" | cat

Output: HOME= USER=


Test 6: Locally assigned variables are also affected.

X=hello; echo $X | cat

Output: (empty)


Test 7: Same with && instead of ;.

X=hello && echo $X | cat

Output: (empty)


Test 8: Same with a newline separator.

X=hello
echo $X | cat

Output: (empty)


Test 9: A preceding no-op command doesn't help.

: && echo $HOME | cat

Output: (empty)


Test 10: A preceding true doesn't help.

true && echo $HOME | cat

Output: (empty)


Test 11: A preceding echo doesn't help.

echo hi && echo $HOME | cat

Output: hi


Test 12: A blank preceding line doesn't help.

 
echo $HOME | cat

Output: (empty)


Test 13: A trailing comment on the same line doesn't help.

echo $HOME | cat # this is a comment

Output: (empty)


Passing cases

Test 14: Adding a comment line before the pipeline makes it work.

# comment
echo $HOME | cat

Output: /home/user


Test 15: Even an empty comment works.

#
echo $HOME | cat

Output: /home/user


Test 16: Wrapping in a for loop works too.

for i in 1; do echo $HOME | cat; done

Output: /home/user


Test 17: Wrapping in if/then.

if true; then echo $HOME | cat; fi

Output: /home/user


Test 18: Using command substitution with printenv (bypasses shell $-expansion entirely).

echo $(printenv HOME) | cat

Output: /home/user


Test 19: Without a pipe, variable expansion works fine.

echo $HOME

Output: /home/user


Test 20: Redirects work fine too.

echo $HOME 2>&1

Output: /home/user


Test 21: env and printenv in a pipe work because they read from the process environment directly, without shell $-expansion.

env | grep HOME

Output: HOME=/home/user

printenv HOME | cat

Output: /home/user


Diagnostic — set -x trace

Test 22: With set -x enabled, the shell prints each command after expansion. The trace reveals that the Bash tool executes commands via eval, and that $HOME has expanded to an empty string before execution.

set -x; echo "$HOME" | cat; set +x

Output:

+(eval):1> echo ''
+(eval):1> cat

+(eval):1> set +x

The (eval):1> prefix confirms the command runs inside eval. The trace shows echo '' — the variable expanded to an empty string.


Test 23: For comparison, the same command with a # comment prefix shows correct expansion.

# comment
set -x; echo "$HOME" | cat; set +x

Output:

+(eval):2> echo /home/user
+(eval):2> cat
/home/user
+(eval):2> set +x

Note (eval):2> instead of (eval):1> — the comment occupies line 1, pushing the command to line 2. The trace shows echo /home/user — the variable expanded correctly.

Claude Model

Not sure / Multiple models

Is this a regression?

Yes, this worked in a previous version

Last Working Version

Best guess: 2.1.50 (but probably later versions still worked too)

Claude Code Version

2.1.62

Platform

Anthropic API

Operating System

Other Linux

Terminal/Shell

iTerm2

Additional Information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:bashbugSomething isn't workinghas reproHas detailed reproduction stepsstaleIssue is inactive

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions