Published on 2026-08-20

Claude Code's Bash tool runs zsh, not fish

I am a long-time user of the fish shell. And I love it.

And I use Claude Code in this shell.

At launch, Claude Code copies $SHELL into its system prompt, so the LLM saw Shell: /opt/homebrew/bin/fish and wrote fish syntax. The Bash tool then choked on set, ; and and for … end, every few sessions, and the LLM retried in bash until something stuck. I asked it to check what it was actually running in:

$ ps -p $$ -o comm=; echo $ZSH_VERSION; [[ -o login ]] && echo login; [[ -o interactive ]] || echo not-interactive/bin/zsh5.9loginnot-interactive

A login, non-interactive zsh. That means ~/.zshenv and ~/.zprofile are read, ~/.zshrc is not. I had neither of the first two. Everything lived in ~/.config/fish/config.fish.

It had worked for months anyway, because Claude Code runs fish once at startup to harvest PATH and the zsh child inherits it. Started from a fish terminal, node and pnpm were there. An isolated login zsh showed what the tool would get from anywhere else:

$ env -i HOME=$HOME zsh -l -c 'echo $PATH'/Users/caillou/.docker/bin:/bin:/usr/bin:/usr/ucb:/usr/local/bin

No brew, no asdf, no rg, no locale. Docker was there only because Docker Desktop had appended itself to a ~/.zshrc I never opened.

The fix was a ~/.zprofile that mirrors the non-interactive parts of my fish config:

# Read by login shells, including Claude Code's non-interactive Bash tool shell.# Mirrors the non-interactive parts of ~/.config/fish/config.fish.eval "$(/opt/homebrew/bin/brew shellenv)"export LC_ALL=en_US.UTF-8export LC_CTYPE=en_US.UTF-8export LANG=en_US.UTF-8export XDG_CONFIG_HOME="$HOME/.config"export PATH="./node_modules/.bin:$PATH:$HOME/.local/bin"# asdf must come after PATH is set. "$HOME/.asdf/asdf.sh"source "$HOME/.docker/init-zsh.sh" || true # Added by Docker Desktopif command -v wt >/dev/null 2>&1; then eval "$(command wt config shell init zsh)"; fi

~/.zshrc shrank to one line, so an interactive non-login zsh gets the same environment without prepending PATH twice:

[[ -o login ]] || source ~/.zprofile

Same test, after:

$ env -i HOME=$HOME zsh -l -c 'command -v node pnpm rg'/Users/caillou/.asdf/shims/node/Users/caillou/.asdf/shims/pnpm/opt/homebrew/bin/rg

I also pinned the shell in ~/.claude/settings.json, so the picker does not have to guess:

"env": {  "CLAUDE_CODE_SHELL": "/bin/zsh"}

That setting changes the executable and nothing else. The prompt still says Shell: fish, so the LLM still reaches for fish syntax. The part that stopped it was ~/.claude/CLAUDE.md:

# ShellThe Bash tool runs zsh 5.9 (login, non-interactive), even though the system prompt reports `Shell: fish`. Write POSIX/zsh syntax, never fish (`set`, `; and`, `for … end` will fail).zsh gotchas:- A word starting with `=` is expanded as `=command`. Never `echo ===x===`; quote it.- Unquoted `$VAR` is not word-split. Use `${=VAR}`, an array, or list items explicitly.- Environment comes from `~/.zprofile` (brew, asdf, locale, `./node_modules/.bin`), not `~/.zshrc`.

The gotchas earned their place. Minutes after confirming it was in zsh, the LLM ran echo ===x=== out of bash habit and got ===== not found.

Why not fish? The shell picker accepts $SHELL only if the path contains bash or zsh. Anything else falls back to searching for zsh, then bash. That comes from a decompiled bundle posted in anthropics/claude-code#7490, open since September 2025. Until that changes, fish users get zsh, and zsh needs a ~/.zprofile.