[FEATURE] Allow users to configure which shell the Bash tool uses or inherit initial shell
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
Claude Code's Bash tool currently uses the system's default shell rather than respecting the shell environment it was launched from.
On macOS, it uses zsh even when explicitly launched with fish, for example, losing access to PATHs, aliases, etc. It's impractical to maintain two shell environments (e.g., ensuring PATHs and ENV are identical) when one is never used interactively.
I use fish for development work in iterm but cannot change the system default via chsh because macOS system updates can fail when anything but zsh is set as the default shell (reported it to Apple years ago, no fix).
Proposed Solution
Claude Code should respect the shell environment it was launched from at the very least. When I run SHELL=/opt/homebrew/bin/fish claude, the Bash tool should use fish shell, not the system default.
Ideally, users could configure their preferred shell via a slash command. The interface could show which shell is actually being used, and commands should execute in that shell environment.
Alternative Solutions
I've tried every workaround I can think of other than chsh because it's not an option. I spent $20 on a claude code session trying to find workarounds, nothing worked.
Priority
Medium - Would be very helpful
Feature Category
Configuration and settings
Use Case Example
This came up today:
- MCP server failing, wanted to delete it, /mcp didn't give me a way
- I asked Claude to do it for me
- The bash tool could not find
claudebecause it was not in the PATH - I told it to use
fish -cwhich worked, but it's dangerous because the root command isfishand not the actual command likerm -f
It would have "just worked" the first time with the right shell.
Additional Context
Example (launched in fish shell in iterm2):
SHELL=/opt/homebrew/bin/fish claude
╭───────────────────────────────────────────────────╮
│ ✻ Welcome to Claude Code! │
│ │
│ /help for help, /status for your current setup │
│ │
│ cwd: /Users/jspiro/Downloads │
╰───────────────────────────────────────────────────╯
> which shell
⏺ Bash(echo $SHELL)
⎿ /bin/zsh
⏺ /bin/zsh```
37 Comments
I really did try to find an existing issue that matched, and I'm deeply surprised I can't find one. This seems like a super-obvious issue. All I could find is https://www.reddit.com/r/ClaudeAI/comments/1lf3x7n/claude_code_different_shell/
Yeah I've been struggling with this too. I use the fish -c workaround but it seems to mean I have to approve every single fish -c 'fd/rg etc ...' command manually
Edit: My new workaround is to use opencode
Same here, it's been quite a struggle.
+1
I need too.
+1 to this!
+1
+1 Would be super helpful when trying to automate
nvmusage.+1 -- at least document the ambiguity somewhere; i had to come here to realize it's actually using the system default shell rather than the user's shell (looks the same to me because i just use zsh) ... so i was assuming it was defaulting to MY shell, and trying to figure out a way to make claude code figure that out at startup and reliably remember it throughout a session; now i realize my premise (and thus the workaround itself) was flawed.
one other possible impact (the one that brought me here investigating): claude itself often assumes bash syntax when trying to write ad-hoc scripts, which can lead to failures, wasted time, and possibly more dangerous consequences. the tool should automatically be aware of what shell its Bash() command is going to invoke, and design its scripts accordingly without the user having to hold its hand here ...
I have
fishas my default shell (set withchsh -s). It doesn't help. Claude Code just ignores it.And it looks like the problem is specific to the fish, because of:
SHELL=/opt/homebrew/bin/fish claude(or justclaudefrom my fish shell which implicitly has SHELL variable), Claude Code uses/bin/zshSHELL=/bin/bash claudeClaude Code actually uses bashDo we have some way to see the actual source code responsible for that logic? I would love to debug this issue.
---
UPD: I took a look at minified code in
@anthropic-ai/claude-codepackage. Some parts of the code mention the following:Based on this, it looks like
bashandzshare only two shells that would work properly. And looks like Claude Code will ignorefishand never gonna use it for running shell commands no matter what.---
Thoughts:
Maybe it is not that critical. If you have some env variables (like PATH) which populated by
fishinit configs, these env vars will be inherited by Claude Code if you run it from fish. Nuance: if you run Claude Code from within VS Code, make sure that VS Code usesfishas a shell when opens terminal within VS Code (Terminal › Integrated › Default Profile) and also ensure thatTerminal › Integrated: Inherit Envis enabled.Not trying to pick a fight over design, BUT: It's really quite surprising that they'd pick a shell long deprecated by Apple, given they all seem to use Macbooks in videos.
zshis not my favorite but is the default now.Calling them
/bashesis also confusing: they're/shells–which MIGHT bebash, might bezsh, might besh, etc. But to its credit, bashes implies bash, I guess.I'm really surprised CC has gone this long without using the user's default shell, let alone having a configuration.
@jspiro
I don't think that this a case. They use
$SHELLenvironment variable. Which is good thing - they respect what user might use. But when they detect it is not POSIX-compatible shell, they fallback to zsh. So you shouldn't see Claude Code using bash until you deliberately run Claude from within bash.But I do agree the naming is really wrong here. It shouldn't say "bash" when "any POSIX compatible shell" meant.
same here
+1
On the mac -- it really should default to "Zsh" (given Apple's move) rather than "Bash", so that things like this don't happen:
But as a whole, it should be user configurable. Or at least auto test / utilize the user $SHELL.
Adding some findings that might help:
env.SHELLin settings.json doesn't workI tried configuring
~/.claude/settings.jsonwith:This does not change which shell executes commands. Running
echo $SHELLinside Claude Code still shows/bin/zsh- so theenv.SHELLsetting isn't being applied at all.However,
SHELL=/opt/homebrew/bin/bash claudedoes work - confirming Claude Code determines the shell at process startup from its inherited environment, not from settings.json.Root cause
Running
stringson the claude binary shows it only overridesSHELLon Windows:On macOS/Linux, Claude Code just uses whatever
$SHELLit inherits when the process starts.macOS quirk
My login shell is fish (
dscl . -read /Users/$USER UserShellreturns/opt/homebrew/bin/fish), andecho $SHELLin a normal terminal correctly shows/opt/homebrew/bin/fish. But inside Claude Code,echo $SHELLshows/bin/zsh. So Claude Code is getting/bin/zshfrom somewhere in its launch chain - possibly from how iTerm2 or macOS initializes the environment before fish starts.Current workaround
Fish alias:
Suggested fix
Either:
env.SHELLfrom settings.json before determining which shell to spawnshellPathsetting that explicitly controls the shell binaryif it did respect your shell wouldn't you want the tool to be called something else? I don't use fish/nushell but i would think that it'd confuse the model if the tool wasn't renamed
Yeah this is such a weird issue. I use Bourne shell and every interaction with the Bash tool shows:
Assuredly that's coming from...the skel dotfile:
/usr/share/skel/dot.shrcwithPresumably this error arises from expecting
bashwhen users prefer other shells. This seems pretty resolutely un-unix.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.
Still needed.
This is still needed.
Related use case: performance
Shell initialization adds ~80ms per command on my machine, but Claude doesn't need aliases, prompts, or interactive features. With
bash --norc, it's ~1.6ms.Benchmarks (macOS, M-series):
| Invocation | Time |
|------------|------|
|
zsh -i(full config) | ~79ms ||
bash --norc| ~1.6ms |Suggestion: Allow the setting to accept flags, not just a path:
```json
{ "shellCommand": "bash --norc" }
This solves both the "use a different shell" and "skip initialization for speed" use cases.
```
While dropping rc-parsing all toghether, another possible workaround is to check on launch:
No matter what I do on Mac, Claude finds a way to launch
/bin/bashrather than the shell my user is set to use/opt/homebrew/bin/bash.It would also be an option to check/parse
$BASH_VERSIONfor similar effect.edit: I do this as my normal bashrc includes completions from homebrew, and spits out a number of errors of the form:
This is needed.
On my windows setup, claude code always use git bash even it was launched from powershell.
This causes path issue when I ask it to run docker.
this is so bad that this is not yet fixed from the number one company in the field. just give us a claude.rc so we can fix it ourselves..also switching to opencode now..
Yeah if only they had some kind of artificial intelligence that could read these comments and open a PR for them…
+1
+1
+1
+1
Not fully on topic, but wow...: TIL I learn that Claude Code seemingly run its shell using my interactive shell setup 🤯
No wonder I've seen some strange things happening. My interactive shell setup is tuned for _human interactive use_. Not scripting and standardized behavior. I obviously would not want a code agent to use that by default? (An interesting experiment, perhaps)
An investigation reveal that the shell used is not actually interactive (
[ -o interactive ]). But it does contain all my aliases and shell functions from.zshrc. (I've double checked that my non-interactive shell setup do not load.zshrc)Claude Code seems to make "snapshot" of my interactive environment ("environment" in a broad sense) and load that.
This make the agent confused when using
mvandcp:I'm puzzled why this was thought to be a good idea. Wouldn't you want Claude to use a very standard environment? But I assume it must also solve some things 🤔 . I also admit my interactive shell setup is likely more exotic than the average one.
I'm not sure I see the net-win in using the users shell either? I mean the tool is even named "Bash", and all the documentation and UI refer to "bash commands".
I'm interested in hearing the rationale for this. I can kinda understand that some users would expect
PATHto be the "interactive" one. I guess there are different philosophizes on whether to keep the non-interactive PATH very clean.---
Anyways - sorry for the rant and perhaps somewhat off-topic comment.
This comment contain an suggestion for a workaround:
That prevent user-configured interactive stuff from leaking in. System rc file would still run though.
or more selectively:
Real-world cost of no PowerShell tool on Windows: a cross-platform repo's workaround inventory
I maintain aitools, a cross-platform CLI and deployment framework for managing AI development tooling, configs, and context across Windows 11 and macOS. It covers tool lifecycle management (evaluation, install, config, update), MDM-ready deployment scripts, Claude Code and Cursor configuration, MCP server setup, shell integration, auxiliary CLI tools, and cross-tool interoperability. macOS is supported with bash, Windows with native PowerShell (5.1 -- the version that ships with Windows). The repo serves both a dev-install path (runtime) and an MDM path (self-contained deploy scripts with build-time embedded content). Claude Code is my primary development tool.
What aitools manages today:
| Category | Items |
|----------|-------|
| CLI tools (installed + maintained) | Claude Code, Vercel CLI, Cursor Agent CLI, Pandoc, pwsh (macOS) |
| MCP servers (configured for Claude Code + Cursor) | Chrome DevTools, Vercel, Webflow |
| Configs deployed |
~/.claude/CLAUDE.md,~/.claude/settings.json(hooks, preferences),~/.claude.json(MCP),~/.cursor/mcp.json,~/.cursor/cli-config.json, shell aliases || Context managed |
.claude/rules/(13 rule files),.cursor/rules/(12 rule files),shared/claude-shared.md(user template with profile interpolation), session archive hooks || Runtimes | Node.js (dependency for Claude Code + MCP) |
| Auxiliary CLI tools |
clip2md-- clipboard-to-markdown converter (HTML clipboard capture, pandoc conversion, optional AI-powered naming) || Under evaluation | Typst (PDF engine for pandoc) |
After ~1 month of building this with Claude Code on both Windows and macOS, here's a concrete accounting of the cost of having no PowerShell tool. The problem goes beyond the shell default -- Claude's training makes it lean heavily on bash idioms (sed/awk for file editing, long inline pipelines instead of temp files, complex string manipulation in bash rather than native tools), and without a PowerShell tool there's no way to natively execute or test Windows code paths during development.
Workaround code and behavioral issues
| Category | Count | Notes |
|----------|-------|-------|
| MINGW/MSYS/CYGWIN dispatch blocks | 19 files, ~20 blocks | Every
.shthat calls a.ps1needs one. Miss one and the Windows code path is silently skipped. ||
powershell.exe -File/-Commandinvocations | 12 | Bash shelling out to PowerShell because the Bash tool can't run PS1 natively ||
cygpath -wpath conversions | 22 | Git Bash paths (/c/repos/...) must be converted for PowerShell ||
InvokeGitwrapper calls | ~15 call sites | Suppress git stderr warnings that crash PowerShell under$ErrorActionPreference=Stop||
powershell.exe Get-Commandfor tool discovery | standing rule |which/command -vonly search Git Bash's PATH subset -- tools installed via winget/scoop/choco are invisible |Beyond the mechanical workarounds, the biggest ongoing cost is fighting the model's bash-first bias on Windows:
.ps1file exists right next to the.shfile, Claude will default to writing bash, running the.shvariant, or suggesting bash-native solutions. I have a standing order -- one of six non-negotiable rules -- dedicated solely to forcing platform-native dispatch. It still gets violated..shcheck scripts on Windows instead of the.ps1variants. The checklist rule files now carry explicit "On Windows:powershell.exe -File ..." reminders in every blockquote header because the model would otherwise reach forbash scripts/check-pre-commit.sh-- which hits an OS guard and silently exits, skipping all PS1 validation.sed/awkfor file editing, long inline pipelines instead of temp files,grepinstead ofSelect-String. These work in Git Bash but produce the wrong muscle memory -- the resulting code doesn't translate to the.ps1variants that actually run on Windows.The dispatch block pattern that every script needs:
Documentation and rules overhead
| File | Lines | Purpose |
|------|-------|---------|
|
reference/claude-code-windows-shell.md| 88 | Entire file exists for this limitation ||
.claude/rules/cross-platform.md| ~70 of 100 | OS guards, PS 5.1 compat, ASCII-only rule, encoding gotchas, quoting patterns || Standing order in shared config | 1 of 6 | "Platform-native dispatch" -- non-negotiable rule for AI sessions |
| Checklist blockquotes (x6 files) | 18 | "On Windows: powershell.exe..." in every pre-commit/pre-push/post-push rule |
I also track 5 upstream issues in a version-dependency registry that gets re-checked on every Claude Code upgrade.
13 bugs that shipped to production
These are just the bugs serious enough to make it into release notes. Countless more were caught mid-session -- wasted tokens, wasted time, and interrupted flow debugging issues that wouldn't exist if the model had a native PowerShell tool.
Windows-specific bugs (no cross-platform test catches these):
| Bug | Root cause |
|-----|-----------|
| PowerShell pipeline mangled non-ASCII to
?(0x3F) | Windows console codepage issue affecting all PS versions on Windows, including pwsh 7. macOS unaffected (UTF-8 locale). ||
.NETclipboard API decoded UTF-8 as Windows-1252, producing mojibake | Windows-specific.NETbehavior, invisible on macOS. ||
git diffCRLF warnings crashed PS1 check scripts |$ErrorActionPreference=Stop+ git stderr. Required permanentInvokeGitwrapper. ||
deploy_configs()missing Windows dispatch -- hook deployment silently failed | New bash function, forgot to replicate MINGW dispatch block. || Executable bits dropped on
.shfiles created on Windows | Write tool on Windows doesn't set Unix executable bit. |PS 5.1 bugs (pwsh 7 on macOS gives false confidence -- Microsoft only ships pwsh 7 cross-platform via Homebrew tap, so macOS testing passes while Windows PS 5.1 fails silently):
| Bug | Root cause |
|-----|-----------|
|
ConvertFrom-Json -AsHashtablesilently failed on PS 5.1, clobbering allsettings.jsondata with empty@{}| PS 6+ parameter. Works on pwsh 7 (macOS), fails only on PS 5.1 (Windows). ||
Join-Pathwith 3+ arguments failed on PS 5.1 | PS 6+ feature (-AdditionalChildPath). Works on pwsh 7 (macOS), fails only on PS 5.1. ||
Set-Content -Encoding UTF8wrote BOM on PS 5.1, breaking JSON parsing | PS version difference: 5.1 writes BOM, 6+ writes NoBOM. pwsh 7 on macOS wouldn't surface this. ||
-replacewith scriptblock (PS 6.1+ syntax) produced garbage paths on PS 5.1 | Works on pwsh 7, silently stringified on PS 5.1. || Em-dash in PS1 string literal broke PS 5.1 parsing (Windows-1252 vs UTF-8) | PS 5.1 reads BOM-free UTF-8 as Windows-1252. pwsh 7 reads UTF-8 correctly on all platforms. |
| 3 more PS 5.1 / path-conversion bugs | All PS 5.1-specific, all undetectable without native Windows PS 5.1 execution. |
The underlying pattern isn't just "code was written in bash" -- it's that the model doesn't naturally think about PowerShell at all. It will ignore explicit rules, skip over existing
.ps1files sitting right next to the.shit's editing, and default to bash patterns even when the user has documented the correct approach. Every one of these 13 bugs was preventable if the model had runpowershell.exe -File script.ps1to functionally test its work -- something it can do from the Bash tool but rarely does unprompted.What workarounds cannot fully solve
powershell.exe -File script.ps1from the Bash tool for functional tests, and we do this for syntax validation. But the model doesn't naturally reach for this -- it treats PS1 scripts as "done" after writing them, without testing the Windows execution path. Every functional test has to be explicitly requested or enforced via rules.%LOCALAPPDATA%, .NET APIs) must shell out to PowerShell from inside a bash script.What would help
A first-class
PowerShelltool on Windows (or at minimum honoringCLAUDE_CODE_SHELL) would eliminate:cygpathcallsInvokeGitwrapper and its 15 call sitesWindows is a second-class platform right now. The workarounds work, but they're a substantial tax on every session, and the model's bash-first training bias means constant vigilance is needed to keep it from reaching for the wrong tool.
---
Environment: Windows 11 Pro for Workstations, Claude Code 2.1.62, PowerShell 5.1 (ships with Windows), Git Bash via Git for Windows.
Related issues: #25558, #5049, #16225, #20453 (closed, no fix)
Hey guys why don't CLAUDE_CODE_SHELL and CLAUDE_CODE_SHELL_PREFIX variables work for you?
A fix is available as a Claude Code plugin: powershell-default
Install:
Adds a native Pwsh tool (shows as
Pwsh(...)in the UI). Commands use PowerShell syntax directly. When enabled, Bash tool is blocked. Works on any OS with PowerShell 7+.PR: https://github.com/anthropics/claude-code/pull/35761
Two workarounds depending on your needs:
1. CLAUDE.md instruction (simplest)
Add to your project's
CLAUDE.md:Claude will follow this for most commands. Not 100% reliable since the LLM sometimes forgets, but covers ~90% of cases.
2. PreToolUse hook (v2.1.85+, reliable)
This uses
updatedInputto rewrite the command before execution. The Bash tool receivesfish -c '...'instead of the original command. Works for single-line commands; multi-line or complex quoting may need adjustments.Neither is a full solution (a native
shellPathconfig would be ideal), but the hook approach covers the most common case of getting fish's PATH and environment into Claude Code's commands.While Claude Code doesn't natively support shell selection, you can work around this with a PreToolUse hook that wraps commands in your preferred shell:
Fish shell wrapper hook:
More practical approach — source fish environment in bash:
Add to your CLAUDE.md:
Or use a PreToolUse hook to auto-wrap in fish:
This detects when Claude tries to use fish-specific syntax and reminds it to wrap the command. Not a full solution (that requires Claude Code to respect
$SHELL), but catches the most common friction points.Aimed at maintainers + thread participants. Markdown-formatted, structured, technical.
Adding a homebrew-bash data point and confirming
CLAUDE_CODE_SHELLworksPosting to (1) reproduce the issue with a non-fish shell, (2) verify that
@antonibertel'searlier mention of
CLAUDE_CODE_SHELLis in fact a working escape hatch, and (3) respectfullyask the maintainers to document it.
Environment
/opt/homebrew/bin/bash(set via
chsh, confirmed withdscl . -read /Users/$USER UserShell)$SHELL=/opt/homebrew/bin/bash✓iTerm → /usr/bin/login → -bash(correct)In other words: at the OS level everything is correctly configured for bash. The shell I
type into in iTerm is bash. The override is happening inside Claude Code, not in my
shell setup.
Reproduction (default behavior)
Inside Claude Code's Bash tool, before any override:
The Bash tool spawns /bin/zsh and exports SHELL=/bin/zsh into the subshell, regardless
of the parent's $SHELL. As a consequence, ~/.bash_profile is never sourced — only the
much smaller ~/.zshrc is loaded, and most of the user's shell environment (PATH additions,
direnv hook, starship init, aliases, completions) is missing for the entire session.
Fix that works
Adding CLAUDE_CODE_SHELL to the env block in ~/.claude/settings.json:
After restart, same checks:
Process tree confirms it: claude → /opt/homebrew/bin/bash. The Bash tool now runs under
homebrew bash 5.3.9 with ~/.bash_profile fully sourced. direnv, starship, custom
PATH, and aliases all behave the same as in an interactive iTerm session.
Thanks to everyone keeping this issue alive — and happy to provide more diagnostic output
or test patches if that helps.
CLAUDE_CODE_SHELLis honored for bash but silently rejected for fish (falls back to zsh)On macOS with fish as the login shell, the Bash tool runs zsh. While digging into this I tested the (undocumented)
CLAUDE_CODE_SHELLenv var. It is read and works for bash — but fish is silently downgraded:|
CLAUDE_CODE_SHELL| Bash tool actually ran in ||---|---|
| (unset) | zsh (default) |
|
/bin/bash| bash — honored ✅ ||
/opt/homebrew/bin/fish| zsh — silently downgraded ❌ |Repro (headless
-pto isolate Bash-tool shell selection;$FISH_VERSION/$BASH_VERSIONare set only by the respective shell):So Claude Code appears to validate the requested shell and refuse fish, falling back to zsh — presumably because the Bash tool emits bash-style command syntax. Net for fish users: there's no way to make the Bash tool use fish; the only options I found are zsh (default) or bash (via
CLAUDE_CODE_SHELL).Environment: Claude Code 2.1.159
---
🤖 This comment was researched and written by Claude (Claude Code, Opus 4.8), based on tests run on my machine, and posted on my behalf.
_Written by Claude (Claude Code), investigating on @Gunther-Schulz's machine and posting at his request._
Adding a data point and a narrower framing that's separable from the configurability feature: the environment header Claude Code injects into the model's own context reports the login shell, not the shell the Bash tool actually executes in. On a fish-login box this actively misleads the agent into writing fish syntax that then fails in the (zsh) shell that really runs the command.
Setup
/etc/passwd→/bin/fish)Shell: /bin/fishsnapshot-zsh-*.shEvidence the mismatch is internal to CC, not a user misconfiguration
Walking the process ancestry,
SHELL=/bin/fishis consistent all the way down to theclaudeprocess itself —/usr/bin/zshappears only in the zsh subprocess CC spawns to run Bash-tool commands:And the shells don't self-set
$SHELL— a seeded marker survives, so that/usr/bin/zshvalue is stamped in by CC when it spawns the execution shell, not by zsh:So the behavior is correct-by-design (fish isn't POSIX; CC rightly falls back to a POSIX shell — same root as #64208). The bug is purely reporting.
The narrow ask
Independent of whether the Bash tool ever becomes configurable (the main request here): the header CC feeds the model should name the shell the Bash tool executes in (the POSIX fallback it picked), not the passwd login shell. Today it says
fish; commands run inzsh. An agent that trusts the header writes fish-syntax commands that fail — and the failure mode is the reverse of intuition, because the reported shell is the one that's never used. This is the original post's "the interface could show which shell is actually being used" sub-point, but the consequence lands on the model, not just the human.