[BUG] `Bash` tool executes the user's login shell (e.g. /bin/zsh) and should thus be called "shell" not "bash"; LLM trips over bash-only idioms
Summary
The Bash tool's description begins "Executes a bash command and returns its output." On macOS with SHELL=/bin/zsh, the command is actually evaluated by the zsh, not bash. The tool name and description therefore assert an interpreter that isn't the one running the command.
This isn't cosmetic: zsh and bash differ in default options that change control flow. The model writes idiomatic bash (because the tool says bash), and the divergence shows up as a failed command with an error that doesn't name the cause.
I hit this organically mid-task, not while hunting for it:
<img width="439" height="153" alt="Image" src="https://github.com/user-attachments/assets/9ecf95b4-02a4-4ff9-a339-e538a3398427" />
It's wrong to call something bash if it's actually zsh. The end user sees bash in the terminal when it's zsh. So Claude saying "I hit a zsh error" is weird when the tool name is "bash". You should call it the "Shell" tool if it's the system shell, not "bash" which is a particular type of shell.
Proof that the interpreter is zsh
$ echo "\$0 = $0"; echo "BASH_VERSION = ${BASH_VERSION:-<unset>}"; echo "ZSH_VERSION = ${ZSH_VERSION:-<unset>}"; ps -o comm= -p $$
$0 = /bin/zsh
BASH_VERSION = <unset>
ZSH_VERSION = 5.9
/bin/zsh
The (eval):1: prefix on shell errors also indicates the command is handed to the shell through eval.
Repro
One Bash tool call:
cd /some/empty/dir
rm -f nope*.txt && echo "FOLLOW-UP RAN"
echo "exit status of the chain: $?"
Actual (via the Bash tool → zsh):
(eval):1: no matches found: nope*.txt
exit status of the chain: 1
Expected (what "Executes a bash command" implies — same line, real bash):
$ /bin/bash -c 'cd /some/empty/dir; rm -f nope*.txt && echo "FOLLOW-UP RAN"; echo "exit status: $?"'
FOLLOW-UP RAN
exit status: 0
zsh's nomatch option is on by default: an unmatched glob is a shell error, so rm never runs and the shell returns nonzero. bash passes the unmatched pattern through literally; rm -f ignores a missing file and exits 0.
Why the current signals aren't sufficient
- The tool description says "bash" in the first sentence and never mentions that the shell is the user's login shell. It does say "the shell is initialized from the user's profile," which is about initialization, not about which interpreter.
- The session environment block does say
Shell: zsh. So the information exists, but it directly contradicts the tool's own name and description, and the contradiction is only resolvable by runningecho $0. - The failure mode gives no hint.
(eval):1: no matches found:doesn't say "this is zsh, bash would have accepted this."
Other defaults that diverge the same way and would produce silently different behavior: unquoted parameter expansion isn't word-split in zsh, arrays are 1-indexed, ** recursive globbing is on, echo flag handling differs, and set -o pipefail semantics/availability differ across versions.
Environment
- Environment Info
- Platform: darwin
- Terminal: xterm-256color
- Version: 2.1.258
- Claude Code 2.1.258
- macOS 26.6.2 (arm64)
SHELL=/bin/zsh, zsh 5.9- Feedback ID: b4915ce3-a6ab-4f3e-82e6-9cf58fabaafb
Related
- #7490 — feature request to configure the Bash tool's shell / inherit the initial shell. That's about choice of shell. This report is narrower: whatever shell is chosen, the tool shouldn't describe itself as bash while running something else, and a shell-level failure shouldn't be indistinguishable from a no-op.
Suggested fix
The name and the interpreter need to agree. Any of these three is coherent — the current combination (called bash, runs zsh) is the only one that isn't:
- Actually use bash. Run the command through
bash -c, keeping the login shell only for deriving profile env. The name becomes true and bash-idiomatic output from the model becomes correct. (This is roughly the #7490 direction and the biggest behavioral change.) - Name it after the real shell. Call the tool
Zsh— or interpolate the detected shell into the name/description — so it says what it runs. - Name it generically. Call it
Shell, with a description that states which interpreter is in use ("Executes a command in the user's login shell, currently/bin/zsh"). This is the cheapest option and keeps one stable tool name across users whose shells differ, while still removing the wrong bash prior.
Options 2 and 3 are documentation-only and would each have prevented this instance.
Independently of which is chosen: annotate the failure. When a command exits nonzero with a recognizably shell-specific error (no matches found:, not valid in this context:), append a one-line note to the tool result naming the shell and the divergence. That converts a confusing silent no-op into an actionable error.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Other differences in zsh:
$PIPESTATUSdoes not exist in zsh — the array is$pipestatus, and it is 1-indexed too, sothe first command's status is
$pipestatus[1].${PIPESTATUS[0]}expands to the emptystring, which turns
exit=${PIPESTATUS[0]}intoexit=and any comparison against it intoa silent pass.
[[ =~ ]]fills$match, not$BASH_REMATCH.${BASH_REMATCH[1]}is silently empty;use
${match[1]}.Additionally there are some other silent failures (at least on macOS):
dateis BSD date (not GNU): every option must precede the operand.date -u -j -v-1d -f %Y-%m-%d "$d" +%Y-%m-%dis correct; a-vplaced after"$d"isignored without an error and yields a full date string instead of the formatted one.
$TMPDIRdiffers inside and outside the sandbox (/tmp/claude-503vs/var/folders/…/T/). Never pass a$TMPDIRpath between a sandboxed command and one runwith
dangerouslyDisableSandbox— use an absolute path both can reach.Adding a workaround that exists today, and a related mismatch it exposes.
Workaround:
CLAUDE_CODE_SHELLCLAUDE_CODE_SHELLis documented and makes the Bash tool run a bash or zsh binary of your choice instead of$SHELL. That is option 1 from the suggested fix, available now as user config. In~/.claude/settings.json:After a restart, the repro from this issue behaves the way the tool name promises (Claude Code 2.1.270, macOS, login shell still
/bin/zsh). OneBashtool call:Output:
Caveats for anyone trying it:
/bin/bash, which is bash 3.2. Use a current bash, e.g. from Homebrew.~/.bashrc(and~/.bash_profile, since it starts a login shell) instead of~/.zshrc. PATH is inherited from the environmentclaudestarts in, but functions and aliases defined only in zsh startup files, such as nvm'snvmfunction, need a bash equivalent.The mismatch it exposes
With the override set, the environment block Claude sees still says
Shell: zsh, while every command runs in bash and even$SHELLinside the tool is/opt/homebrew/bin/bash. The issue notes that the environment block is currently the only place the real shell is named; withCLAUDE_CODE_SHELLset, that line is wrong in the other direction.In 2.1.270 the environment block derives the name from
$SHELLalone, while the executor checksCLAUDE_CODE_SHELLfirst:Whichever naming option is chosen for the tool, the
Shell:line should come from the same resolution the executor uses, so it names the shell that actually runs the commands.Confirming this, with a worse failure mode than the examples above: the tool reports success while silently doing nothing.
macOS (Darwin 25.6.0), login shell
/bin/zsh. A subagent wrote a load generator:kill $pidsis correct bash. Under zsh,$pidsexpands to a single word,killreturns 1, and2>/dev/nulldiscards the only evidence. The trailing command then succeeds, so the tool result wasis_error: false. The backgrounded loops reparented to PPID 1 and kept spinning — 40 accumulated across five runs.Two things the examples above don't cover:
no matches found, empty$PIPESTATUS). Here the model saw a green result and believed cleanup ran, so there was no signal to correct against.run_in_background(#89625).Same mechanism as #74888 (
for rp in $listcollapsing to one token under zsh), which was closed NOT_PLANNED but scoped to the Monitor tool's own example scripts rather than model-authored commands.