Let the Bash tool run commands under bash, not the user's login shell (zsh)
Problem
My login shell is zsh, and the Bash tool runs commands under zsh. But the tool is called "Bash" and Claude (plus most snippets it writes) assumes bash semantics, so commands routinely break on bash-vs-zsh differences:
- an unmatched glob is FATAL in zsh (
zsh: no matches found) and aborts the whole command, instead of empty-expanding as in bash — this silently false-negatives file-existence checks - unquoted
$vardoes not word-split in zsh - bash associative arrays (
declare -A,${!m[@]}) are unsupported
These fail intermittently and are hard to prevent with prompt/CLAUDE.md rules alone.
What I found
There is no setting to control the shell the Bash tool uses. defaultShell only affects interactive ! commands, not agent tool execution. Env vars only cover timeouts/output limits (BASH_DEFAULT_TIMEOUT_MS, etc.), not shell selection.
Requested
A first-class way to make the Bash tool use bash regardless of the login shell, e.g. one of:
- a settings.json key like
bashShell/toolShell(path to the shell binary), or - an env var (e.g.
CLAUDE_BASH_SHELL=/bin/bash), or - an option to run every Bash-tool command via
bash -cautomatically.
Ideally it still inherits the interactive shell's PATH/env (sourced from .zshrc), so tools installed via the login shell keep working — i.e. a bash child inside the existing environment, not a full shell swap.
Also: please document explicitly whether Bash-tool commands run in $SHELL or always in /bin/bash — the tools reference is ambiguous on this.
Workaround today
Prefixing non-trivial commands with bash -c '...' works and inherits PATH, but it relies on Claude remembering to do it every time.
3 Comments
A concrete instance from today (macOS, zsh login shell) for the "unmatched glob is fatal" bullet — with a wrinkle worth adding: it wasn't a filename glob, it was a flag value.
zsh tries to expand
--include=*.tsxas a glob against the cwd, finds no.tsxfiles there (they live undersrc/), and aborts the whole command:Under bash the unmatched pattern passes through literally and
grepreceives the flag it was meant to get.Why this instance is worth citing: the failure presented as a false negative that looked like a real answer. The command ran inside a loop over several API names, so the output was a tidy table of
0counts interleaved with the zsh errors — readable as "this codebase uses none of these APIs", which was the exact opposite of the truth. The question being answered was whether a React major upgrade was safe, so a silent zero was the worst possible failure shape.Quoting (
--include="*.tsx") fixes it, but nothing about the token looks glob-shaped to someone writing the command, which is why it keeps getting reintroduced regardless of prompt-level rules — matching the point in the original report that this can't realistically be prevented via CLAUDE.md.Adding a vote for option 3 (run every Bash-tool command via
bash -cby default, inheriting the login shell's env), since it fixes the whole class rather than one syntax at a time.You can already pin the Bash tool to bash: set the
CLAUDE_CODE_SHELLenvironment variable to a bash binary, e.g.CLAUDE_CODE_SHELL=/bin/bash claude(or put it underenvin your settings.json). It accepts a path to abashorzshbinary and overrides the$SHELL-based auto-detection, while your PATH and environment are still inherited.Docs: https://code.claude.com/docs/en/env-vars
Closing since this is available today. Feel free to reopen if it doesn't cover your case.
🤖 Generated with Claude Code
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.