[FEATURE] Allow users to configure which shell the Bash tool uses or inherit initial shell

Open 💬 37 comments Opened Sep 12, 2025 by jspiro

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 claude because it was not in the PATH
  • I told it to use fish -c which worked, but it's dangerous because the root command is fish and not the actual command like rm -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```

View original on GitHub ↗

37 Comments

jspiro · 10 months ago

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/

riordanpawley · 10 months ago

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

KaramanisDev · 10 months ago

Same here, it's been quite a struggle.

blo-grindr · 9 months ago

+1

ppx123-web · 9 months ago

I need too.

cameronk · 9 months ago

+1 to this!

kanghyojun · 8 months ago

+1

alvincrespo · 8 months ago

+1 Would be super helpful when trying to automate nvm usage.

guppy-jpf · 8 months ago

+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 ...

DanilAgafonov · 8 months ago

I have fish as my default shell (set with chsh -s). It doesn't help. Claude Code just ignores it.

And it looks like the problem is specific to the fish, because of:

  • when I run SHELL=/opt/homebrew/bin/fish claude (or just claude from my fish shell which implicitly has SHELL variable), Claude Code uses /bin/zsh
  • when I run SHELL=/bin/bash claude Claude Code actually uses bash

Do 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-code package. Some parts of the code mention the following:

function Rm8(){let A=(C)=>{try{return a8Q(`which ${C}`,{stdio:["ignore","pipe","ignore"]}).toString().trim()}catch{return null}},B=process.env.SHELL,Q=B&&(B.includes("bash")||B.includes("zsh")),I=B?.includes("bash"),G=A("zsh"),Z=A("bash"),Y=["/bin","/usr/bin","/usr/local/bin","/opt/homebrew/bin"],W=(I?["bash","zsh"]:["zsh","bash"]).flatMap((C)=>Y.map((F)=>`${F}/${C}`));if(I){if(Z)W.unshift(Z);if(G)W.push(G)}else{if(G)W.unshift(G);if(Z)W.push(Z)}if(Q&&n8Q(B))W.unshift(B);let X=W.find((C)=>C&&n8Q(C));if(!X){let C="No suitable shell found. Claude CLI requires a Posix shell environment. Please ensure you have a valid shell installed and the SHELL environment variable set.";throw JA(Error(C),TJ0),Error(C)}return X}

Based on this, it looks like bash and zsh are only two shells that would work properly. And looks like Claude Code will ignore fish and 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 fish init 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 uses fish as a shell when opens terminal within VS Code (Terminal › Integrated › Default Profile) and also ensure that Terminal › Integrated: Inherit Env is enabled.

jspiro · 8 months ago

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. zsh is not my favorite but is the default now.

Calling them /bashes is also confusing: they're /shells–which MIGHT be bash, might be zsh, might be sh, 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.

braska · 8 months ago

@jspiro

they'd pick a shell long deprecated by Apple

I don't think that this a case. They use $SHELL environment 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.

mxvsh · 8 months ago

same here

ventz · 7 months ago

+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:

"Command: "shopt -u extglob" not found!"

But as a whole, it should be user configurable. Or at least auto test / utilize the user $SHELL.

AbdelrahmanHafez · 7 months ago

Adding some findings that might help:

env.SHELL in settings.json doesn't work

I tried configuring ~/.claude/settings.json with:

{
  "env": {
    "SHELL": "/opt/homebrew/bin/bash"
  }
}

This does not change which shell executes commands. Running echo $SHELL inside Claude Code still shows /bin/zsh - so the env.SHELL setting isn't being applied at all.

However, SHELL=/opt/homebrew/bin/bash claude does work - confirming Claude Code determines the shell at process startup from its inherited environment, not from settings.json.

Root cause

Running strings on the claude binary shows it only overrides SHELL on Windows:

jvA=()=>{if(BB()==="windows"){let R=lET();process.env.SHELL=R...

On macOS/Linux, Claude Code just uses whatever $SHELL it inherits when the process starts.

macOS quirk

My login shell is fish (dscl . -read /Users/$USER UserShell returns /opt/homebrew/bin/fish), and echo $SHELL in a normal terminal correctly shows /opt/homebrew/bin/fish. But inside Claude Code, echo $SHELL shows /bin/zsh. So Claude Code is getting /bin/zsh from somewhere in its launch chain - possibly from how iTerm2 or macOS initializes the environment before fish starts.

Current workaround

Fish alias:

alias claude 'SHELL=/opt/homebrew/bin/bash command claude'

Suggested fix

Either:

  1. Read and apply env.SHELL from settings.json before determining which shell to spawn
  2. Add a dedicated shellPath setting that explicitly controls the shell binary
rekram1-node · 7 months ago

if 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

sgharms · 6 months ago

Yeah this is such a weird issue. I use Bourne shell and every interaction with the Bash tool shows:

● Bash(ls -la
      /home/heraclitus/git_checkouts/nextspace/Installer/FreeBSD/KNOWN_ISSUES.md)
  ⎿  Error: Exit code 1
     /home/heraclitus/.profile: line 33: bind: warning: line editing not enabled
     readline: ^[[A: no key sequence terminator
     readline: ed-search-prev-history: no key sequence terminator
     /home/heraclitus/.profile: line 34: bind: warning: line editing not enabled
     readline: ^[[B: no key sequence terminator
     readline: ed-search-next-history: no key sequence terminator
     /home/heraclitus/.profile: line 37: bind: warning: line editing not enabled
     readline: \e[1;5C: no key sequence terminator
     readline: em-next-word: no key sequence terminator

Assuredly that's coming from...the skel dotfile: /usr/share/skel/dot.shrc with

# # csh like history on arrow up and down
bind ^[[A ed-search-prev-history
bind ^[[B ed-search-next-history

# # Ctrl+right arrow: go to the next word
# # Ctrl+left arrow: go to the previous word
bind "\\e[1;5C" em-next-word
bind "\\e[1;5D" ed-prev-word

Presumably this error arises from expecting bash when users prefer other shells. This seems pretty resolutely un-unix.

github-actions[bot] · 5 months ago

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.

AbdelrahmanHafez · 5 months ago

Still needed.

mac-tron · 5 months ago

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.
```

e12e · 5 months ago

While dropping rc-parsing all toghether, another possible workaround is to check on launch:

# ~/.bashrc
if [ "${CLAUDECODE:-0}" == "1" ]; then
	. ~/.bashrc_claude
else
	# echo "sourcing regular bash rc"
	. ~/.bashrc_real
fi

No matter what I do on Mac, Claude finds a way to launch /bin/bash rather than the shell my user is set to use /opt/homebrew/bin/bash.

It would also be an option to check/parse $BASH_VERSION for similar effect.

edit: I do this as my normal bashrc includes completions from homebrew, and spits out a number of errors of the form:

bash: _comp_deprecate_func: command not found
#(...)
bash: _comp_deprecate_var: command not found

bash: declare: -A: invalid option
declare: usage: declare [-afFirtx] [-p] [name[=value] ...]
bash: complete: nosort: invalid option name
naivej · 5 months ago

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.

tracure1337 · 5 months ago

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..

jspiro · 5 months ago

Yeah if only they had some kind of artificial intelligence that could read these comments and open a PR for them…

luquibu · 5 months ago

+1

faranahmadk · 5 months ago

+1

dinoluck · 4 months ago

+1

techjoec · 4 months ago

+1

olejorgenb · 4 months ago

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.

cat /home/ole/.claude/shell-snapshots/snapshot-zsh-1771742273724-jpbryn.sh  | grep mv=
alias -- mv='mv -i'

This make the agent confused when using mv and cp:

cp: 'overwrite /home/ole/.../testing/bar' ?

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 PATH to 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:

# Top of .zshrc
if [[ -n "$CLAUDECODE" ]]; then
    return
fi

That prevent user-configured interactive stuff from leaking in. System rc file would still run though.

or more selectively:

if [[ -z "$CLAUDECODE" ]]; then
  alias -- -='cd -'
fi
nobul-jose · 4 months ago

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 .sh that calls a .ps1 needs one. Miss one and the Windows code path is silently skipped. |
| powershell.exe -File / -Command invocations | 12 | Bash shelling out to PowerShell because the Bash tool can't run PS1 natively |
| cygpath -w path conversions | 22 | Git Bash paths (/c/repos/...) must be converted for PowerShell |
| InvokeGit wrapper calls | ~15 call sites | Suppress git stderr warnings that crash PowerShell under $ErrorActionPreference=Stop |
| powershell.exe Get-Command for tool discovery | standing rule | which/command -v only 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:

  • Claude constantly wants to write bash for Windows. Even when a .ps1 file exists right next to the .sh file, Claude will default to writing bash, running the .sh variant, 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.
  • Claude runs .sh check scripts on Windows instead of the .ps1 variants. The checklist rule files now carry explicit "On Windows: powershell.exe -File ..." reminders in every blockquote header because the model would otherwise reach for bash scripts/check-pre-commit.sh -- which hits an OS guard and silently exits, skipping all PS1 validation.
  • Claude reaches for bash idioms that are wrong for Windows. sed/awk for file editing, long inline pipelines instead of temp files, grep instead of Select-String. These work in Git Bash but produce the wrong muscle memory -- the resulting code doesn't translate to the .ps1 variants that actually run on Windows.
  • Rules and docs get ignored under context pressure. Even with 70+ lines of cross-platform rules, a standing order, and dispatch reminders in every checklist, Claude will still skip the PS1 path when working through a long task. The rules are in context but get deprioritized against feature logic.

The dispatch block pattern that every script needs:

case "$(uname -s)" in
    MINGW*|MSYS*|CYGWIN*)
        powershell.exe -NoProfile -ExecutionPolicy Bypass \
            -File "$(cygpath -w "$ps1_path")" ;;
    *) bash "$sh_path" ;;
esac

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). |
| .NET clipboard API decoded UTF-8 as Windows-1252, producing mojibake | Windows-specific .NET behavior, invisible on macOS. |
| git diff CRLF warnings crashed PS1 check scripts | $ErrorActionPreference=Stop + git stderr. Required permanent InvokeGit wrapper. |
| deploy_configs() missing Windows dispatch -- hook deployment silently failed | New bash function, forgot to replicate MINGW dispatch block. |
| Executable bits dropped on .sh files 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 -AsHashtable silently failed on PS 5.1, clobbering all settings.json data with empty @{} | PS 6+ parameter. Works on pwsh 7 (macOS), fails only on PS 5.1 (Windows). |
| Join-Path with 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 UTF8 wrote 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. |
| -replace with 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 .ps1 files sitting right next to the .sh it'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 run powershell.exe -File script.ps1 to functionally test its work -- something it can do from the Bash tool but rarely does unprompted.

What workarounds cannot fully solve

  1. Functional testing requires constant prompting. Claude can run powershell.exe -File script.ps1 from 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.
  1. Hooks run in bash on Windows, permanently. The hook execution context is hardcoded to bash. Any hook that needs Windows-specific work (registry, %LOCALAPPDATA%, .NET APIs) must shell out to PowerShell from inside a bash script.

What would help

A first-class PowerShell tool on Windows (or at minimum honoring CLAUDE_CODE_SHELL) would eliminate:

  • All 20 dispatch blocks and 22 cygpath calls
  • The standing order, the 88-line reference doc, and ~70 lines of cross-platform rules
  • The entire class of "authored in bash, broke in PS 5.1" bugs
  • The InvokeGit wrapper and its 15 call sites
  • The inability to functionally test PS1 scripts during development

Windows 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)

antonibertel · 4 months ago

Hey guys why don't CLAUDE_CODE_SHELL and CLAUDE_CODE_SHELL_PREFIX variables work for you?

cruzlauroiii · 4 months ago

A fix is available as a Claude Code plugin: powershell-default

Install:

/plugin marketplace add cruzlauroiii/claude-code
/plugin install powershell-default@cruzlauroiii-plugins

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

yurukusa · 3 months ago

Two workarounds depending on your needs:
1. CLAUDE.md instruction (simplest)
Add to your project's CLAUDE.md:

When executing commands, wrap them in `fish -c '...'` to use the fish shell and its PATH/env configuration.

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)

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
[ -z "$COMMAND" ] && exit 0
echo "$COMMAND" | grep -q '^fish -c' && exit 0
echo "$COMMAND" | grep -qE '^(cd|echo|cat|ls|pwd|true|false)\b' && exit 0
ESCAPED=$(printf '%s' "$COMMAND" | sed "s/'/'\\\\''/g")
jq -n --arg cmd "fish -c '$ESCAPED'" '{
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "allow",
    updatedInput: { command: $cmd }
  }
}'
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{"type": "command", "command": "bash ~/.claude/hooks/fish-shell-wrapper.sh"}]
    }]
  }
}

This uses updatedInput to rewrite the command before execution. The Bash tool receives fish -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 shellPath config would be ideal), but the hook approach covers the most common case of getting fish's PATH and environment into Claude Code's commands.

yurukusa · 3 months ago

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:

#!/bin/bash
# ~/.claude/hooks/fish-shell-wrapper.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$COMMAND" ] && exit 0

# Inform Claude that fish is the target shell
echo "NOTE: This command will be executed in your default shell. If you need fish-specific syntax (e.g., 'set -x' instead of 'export'), adjust accordingly." >&2
exit 0

More practical approach — source fish environment in bash:

Add to your CLAUDE.md:

## Shell Environment
My shell is fish. When running commands:
- Use `fish -c "command"` for fish-specific syntax
- For PATH: run `fish -c 'echo $PATH'` instead of `echo $PATH`
- For environment vars: use `env` which works in both shells

Or use a PreToolUse hook to auto-wrap in fish:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "cat | jq -r '.tool_input.command // empty' | grep -qE '^(set -x|fish|abbr|funced)' && echo 'NOTE: Detected fish syntax. Claude uses bash — wrap in: fish -c \"command\"' >&2; exit 0"
          }
        ]
      }
    ]
  }
}

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.

carlos-andres · 2 months ago

Aimed at maintainers + thread participants. Markdown-formatted, structured, technical.

Adding a homebrew-bash data point and confirming CLAUDE_CODE_SHELL works

Posting to (1) reproduce the issue with a non-fish shell, (2) verify that @antonibertel's
earlier mention of CLAUDE_CODE_SHELL is in fact a working escape hatch, and (3) respectfully
ask the maintainers to document it.

Environment

  • macOS Sequoia (Darwin 24.6.0)
  • Claude Code 2.1.119
  • Login shell at directory-services level: /opt/homebrew/bin/bash

(set via chsh, confirmed with dscl . -read /Users/$USER UserShell)

  • Interactive iTerm session: $SHELL=/opt/homebrew/bin/bash
  • Login process chain: 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:

$ ps -p $$ -o comm=
/bin/zsh

$ echo "SHELL=$SHELL  BASH_VERSION=$BASH_VERSION  ZSH_VERSION=$ZSH_VERSION"
SHELL=/bin/zsh  BASH_VERSION=  ZSH_VERSION=5.9

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:

{
  "env": {
    "CLAUDE_CODE_SHELL": "/opt/homebrew/bin/bash",     
    "BASH_SILENCE_DEPRECATION_WARNING": "1"
  }
}

After restart, same checks:

$ ps -p $$ -o comm=
/opt/homebrew/bin/bash

$ echo "SHELL=$SHELL  BASH_VERSION=$BASH_VERSION  BASH=$BASH"
SHELL=/opt/homebrew/bin/bash  BASH_VERSION=5.3.9(1)-release  BASH=/opt/homebrew/bin/bash

$ type direnv
direnv is /opt/homebrew/bin/direnv

$ echo "$STARSHIP_SHELL"
bash

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.

domon-envato · 1 month ago

CLAUDE_CODE_SHELL is 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_SHELL env 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 -p to isolate Bash-tool shell selection; $FISH_VERSION/$BASH_VERSION are set only by the respective shell):

# fish requested -> falls back to zsh
env CLAUDE_CODE_SHELL=/opt/homebrew/bin/fish claude -p \
  'Use the Bash tool to run: echo "SHELL=$SHELL FISH_VER=$FISH_VERSION" > /tmp/x' \
  --dangerously-skip-permissions
# /tmp/x  ->  SHELL=/bin/zsh FISH_VER=        (not fish)

# bash requested -> honored
env CLAUDE_CODE_SHELL=/bin/bash claude -p \
  'Use the Bash tool to run: echo "SHELL=$SHELL BASH_VER=$BASH_VERSION" > /tmp/x' \
  --dangerously-skip-permissions
# /tmp/x  ->  SHELL=/bin/bash BASH_VER=3.2.57(1)-release   (ran in bash)

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.

Gunther-Schulz · 1 month ago

_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

  • Arch/CachyOS, terminal = ghostty, login shell = fish (/etc/passwd/bin/fish)
  • Session header CC shows the model: Shell: /bin/fish
  • Bash-tool execution actually happens in zsh 5.9.1; snapshots on disk are all snapshot-zsh-*.sh

Evidence the mismatch is internal to CC, not a user misconfiguration

Walking the process ancestry, SHELL=/bin/fish is consistent all the way down to the claude process itself — /usr/bin/zsh appears only in the zsh subprocess CC spawns to run Bash-tool commands:

zsh     -> SHELL=/usr/bin/zsh   <- only here
claude  -> SHELL=/bin/fish
fish    -> SHELL=/bin/fish
ghostty -> SHELL=/bin/fish

And the shells don't self-set $SHELL — a seeded marker survives, so that /usr/bin/zsh value is stamped in by CC when it spawns the execution shell, not by zsh:

$ env SHELL=/MARKER zsh  -c 'echo $SHELL'   -> /MARKER
$ env SHELL=/MARKER bash -c 'echo $SHELL'   -> /MARKER

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 in zsh. 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.