Skip to content

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

Open

Description

@jspiro

Preflight Checklist

  • I have searched existing requests and this feature hasn't been requested yet
  • 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```

Activity

  1. jspiro commented on Sep 12, 2025

    @jspiro
    Author

    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/

  2. riordanpawley commented on Sep 12, 2025

    @riordanpawley

    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

  3. KaramanisDev commented on Sep 15, 2025

    @KaramanisDev

    Same here, it's been quite a struggle.

  4. blo-grindr commented on Oct 4, 2025

    @blo-grindr

    +1

  5. ppx123-web commented on Oct 9, 2025

    @ppx123-web

    I need too.

  6. cameronk commented on Oct 16, 2025

    @cameronk

    +1 to this!

  7. kanghyojun commented on Oct 20, 2025

    @kanghyojun

    +1

  8. alvincrespo commented on Oct 24, 2025

    @alvincrespo

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

  9. guppy-jpf commented on Oct 31, 2025

    @guppy-jpf

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

  10. DanilAgafonov commented on Oct 31, 2025

    @DanilAgafonov

    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.

  11. jspiro commented on Nov 6, 2025

    @jspiro
    Author

    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.

  12. 26 remaining items

  13. yurukusa commented on Mar 27, 2026

    @yurukusa

    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.

  14. yurukusa commented on Mar 30, 2026

    @yurukusa

    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.

  15. carlos-andres commented on May 7, 2026

    @carlos-andres

    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.

  16. domon-envato commented on Jun 1, 2026

    @domon-envato

    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.

  17. Gunther-Schulz commented on Jun 3, 2026

    @Gunther-Schulz

    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.

  18. andylizf commented on Aug 31, 2026

    @andylizf

    Two things not yet in this thread: a failure mode that needs no mistake from the model, and what it costs in practice.

    The mismatch bites even when the model writes correct syntax. The reports above are about the model emitting fish constructs. There is a second mode: grep -rn "..." --include=*.py src/ is valid bash. Under zsh the unquoted glob matches nothing in the working directory, and zsh aborts the whole command before grep ever runs:

    (eval):1: no matches found: --include=*.py
    

    Nothing was written wrong for the shell the model was told it had. A different shell simply refused it. Quoting the pattern fixes it, but an agent has no reason to quote when it believes zsh is not involved.

    This also suggests the header is not merely stale. Shell selection drops a login shell it does not support and falls back to one it does; the header does not apply that same rule, so it announces a shell that selection had already rejected.

    Frequency, from one machine's transcripts. About 30 days of local session logs on a fish-login macOS box: no matches found appears 1729 times across 388 sessions, roughly 514 of them --include=*.<ext>, and most of the rest unquoted paths or paths containing spaces. Each is a wasted tool call plus a retry.

    Setting CLAUDE_CODE_SHELL to bash removes the class locally, so the workaround is real. The header stays wrong, and that part is only fixable upstream.

  19. ririshi commented on Oct 23, 2026

    @ririshi

    I run fish on CachyOS and my Claude just created a bunch of empty directories because it wrote some bash code that was apparently also valid in zsh, but did not do the same thing at all.

    Luckily, another agent picked up on the mistake later and was able to delete the directories without issue, but it does seem like a very unnecessary problem.

    Just default to bash, also when zsh is installed. And tell the agent what tool it's actually using in the system prompt.

  20. guppy-jpf commented on Oct 26, 2026

    @guppy-jpf

    Just default to bash, also when zsh is installed

    this is not an optimal solution on macos because the bash there (for licensing reasons) is not up-to-date.
    'just default to bash' means CC has to either:

    • know what version of bash is present,
    • dumb itself down for compatibility, ignoring modern features just for consistency's sake (which will not make bash v4 or v5 users happy),
    • or risk generating incorrect commands on mac (which is what used to happen all the time) ...

    instead, it should just be aware of what shell the user's account is set to, and formulate commands for that shell (but that's the same as the first item in the list). i suppose it's actually not an easy fix. the uncanny valley problem leads us to expect more of these tools than is actually reasonable at this point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions