Bash tool returns empty output when executing shell script files on Windows (MINGW64)

Open 💬 41 comments Opened Jan 17, 2026 by Daichi-Kudo

Description

On Windows with MINGW64 (Git Bash), the Bash tool returns empty output ("No content") when executing shell script files. This affects common commands like docker, pnpm, and npm which use shell script wrappers on Windows.

Environment

  • OS: Windows 11 (Build 26200.7623)
  • Shell: MINGW64 (Git Bash)
  • Claude Code version: Latest

Steps to Reproduce

  1. Create a simple shell script:
cat > /tmp/test.sh << 'EOF'
#!/bin/sh
echo "hello world"
EOF
chmod +x /tmp/test.sh
  1. Execute the script via Bash tool:
/tmp/test.sh
# Returns: (No content)

bash /tmp/test.sh
# Returns: (No content)

sh /tmp/test.sh
# Returns: (No content)
  1. Compare with inline execution:
sh -c 'echo "hello world"'
# Returns: hello world (works correctly)

Affected Commands

| Command | Type | Result |
|---------|------|--------|
| docker | Shell script wrapper | (No content) |
| pnpm | Shell script wrapper | (No content) |
| ./script.sh | Direct script execution | (No content) |
| bash script.sh | Explicit bash invocation | (No content) |
| sh script.sh | Explicit sh invocation | (No content) |

Working Alternatives

| Command | Type | Result |
|---------|------|--------|
| docker.exe | Windows executable | ✓ Works |
| pnpm.cmd | CMD wrapper | ✓ Works |
| sh -c 'inline command' | Inline execution | ✓ Works |
| . script.sh (source) | Current shell | ✓ Works |
| eval "$(cat script.sh)" | Eval | ✓ Works |

Root Cause Analysis

The issue appears to be in how Claude Code's Bash tool captures stdout when:

  1. A new subprocess is spawned to execute a script file
  2. The environment is MINGW64 on Windows

Inline commands (sh -c '...') work because they don't spawn a separate process for file execution. Direct .exe and .cmd files work because they bypass the shell script layer.

Impact

This significantly impacts Windows users who rely on common tools that use shell script wrappers:

  • Docker CLI
  • Node.js package managers (pnpm, npm scripts)
  • Any npm-installed global CLI tools

Suggested Fix

Consider one of these approaches:

  1. Detect MINGW64 environment and adjust subprocess handling
  2. Use different output capture mechanism for script file execution on Windows
  3. Document workaround for Windows users (use .exe/.cmd suffixes)

Workarounds

Users can work around this by:

  1. Using explicit .exe or .cmd suffixes: docker.exe, pnpm.cmd
  2. Running commands through WSL: wsl -d Ubuntu -e sh -c "docker ps"
  3. Using source or eval for simple scripts

View original on GitHub ↗

41 Comments

github-actions[bot] · 6 months ago

Found 2 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/18469
  2. https://github.com/anthropics/claude-code/issues/18670

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

Daichi-Kudo · 6 months ago

Workaround Solution

I've implemented a working workaround using BASH_ENV to automatically load shell functions that redirect commands to their .exe/.cmd equivalents.

Setup

1. Create the workaround script:

mkdir -p ~/.claude/shell-fixes
cat > ~/.claude/shell-fixes/mingw64-workarounds.sh << 'EOF'
#!/bin/bash
# Claude Code MINGW64 Workarounds
# Fix for "No content" issue with shell script wrappers

case "$MSYSTEM" in
    MINGW*|MSYS*)
        # Docker
        docker() { docker.exe "$@"; }
        docker-compose() { docker-compose.exe "$@"; }

        # Node.js package managers
        pnpm() { pnpm.cmd "$@"; }
        npm() { npm.cmd "$@"; }
        npx() { npx.cmd "$@"; }
        yarn() { yarn.cmd "$@"; }

        # Common tools
        tsc() { tsc.cmd "$@"; }
        eslint() { eslint.cmd "$@"; }
        prettier() { prettier.cmd "$@"; }
        vitest() { vitest.cmd "$@"; }
        vite() { vite.cmd "$@"; }

        # Python tools
        python() { python.exe "$@"; }
        pip() { pip.exe "$@"; }
        uv() { uv.exe "$@"; }
        ruff() { ruff.exe "$@"; }
        ;;
esac
EOF

2. Add to Claude Code settings (~/.claude/settings.json):

{
  "env": {
    "BASH_ENV": "C:/Users/YOUR_USERNAME/.claude/shell-fixes/mingw64-workarounds.sh"
  }
}

3. (Optional) Set Windows environment variable for other tools:

[Environment]::SetEnvironmentVariable("BASH_ENV", "$env:USERPROFILE\.claude\shell-fixes\mingw64-workarounds.sh", "User")

How it works

  • BASH_ENV is sourced by bash for non-interactive shells
  • The script defines shell functions that override the problematic commands
  • Functions call .exe or .cmd versions directly, bypassing shell script wrappers
  • Since functions run in the current shell context (not as subprocesses), stdout is captured correctly

Verified working

After implementing this workaround:

  • docker version
  • docker ps
  • pnpm --version
  • npm --version

This is a temporary solution until the root cause is fixed in Claude Code.

Daichi-Kudo · 6 months ago

Updated: Dynamic Workaround Solution (v2)

The previous static workaround required manual updates. Here's an improved dynamic detection approach that automatically handles all affected commands.

How it works

  1. Scans common tool directories (npm global, Docker, etc.)
  2. Detects shell script wrappers that have .exe/.cmd equivalents
  3. Auto-generates wrapper functions at shell startup
  4. Caches results for 1 hour to avoid repeated scans
  5. Provides utility commands for maintenance

Setup

1. Create the workaround script:

Save to ~/.claude/shell-fixes/mingw64-workarounds.sh:

#!/bin/bash
# Claude Code MINGW64 Workarounds - Dynamic Detection
# Issue: https://github.com/anthropics/claude-code/issues/18748

[[ "$MSYSTEM" != MINGW* && "$MSYSTEM" != MSYS* ]] && return 0
[[ -n "$__MINGW64_WORKAROUNDS_LOADED" ]] && return 0
export __MINGW64_WORKAROUNDS_LOADED=1

__to_unix_path() {
    echo "$1" | sed -e 's|\|/|g' -e 's|^\([A-Za-z]\):|/\L\1|'
}

__HOME="$(__to_unix_path "$USERPROFILE")"
[[ -z "$__HOME" ]] && __HOME="$HOME"
__WORKAROUND_CACHE="$__HOME/.claude/shell-fixes/.cache"
__WORKAROUND_CACHE_TTL=3600
__APPDATA_UNIX="$(__to_unix_path "$APPDATA")"
__LOCALAPPDATA_UNIX="$(__to_unix_path "$LOCALAPPDATA")"

__SCAN_DIRS=(
    "/c/Program Files/Docker/Docker/resources/bin:exe"
    "${__APPDATA_UNIX}/npm:cmd"
    "${__LOCALAPPDATA_UNIX}/pnpm:cmd"
    "${__HOME}/.local/bin:exe"
    "/c/ProgramData/chocolatey/bin:exe"
)

__EXCLUDE_COMMANDS=(
    "sh" "bash" "env" "cat" "ls" "cp" "mv" "rm" "mkdir" "grep" "sed" "awk"
    "find" "which" "where" "cmd" "powershell" "pwsh"
)

__should_exclude() {
    local cmd="$1"
    for excl in "${__EXCLUDE_COMMANDS[@]}"; do
        [[ "$cmd" == "$excl" ]] && return 0
    done
    return 1
}

__is_shell_script() {
    local file="$1"
    [[ ! -f "$file" ]] && return 1
    local first_bytes=$(head -c 2 "$file" 2>/dev/null)
    [[ "$first_bytes" == "#!" ]] && return 0
    return 1
}

__generate_wrapper() {
    local cmd="$1" ext="$2"
    eval "${cmd}() { ${cmd}.${ext} \"\$@\"; }"
    export -f "$cmd" 2>/dev/null
}

__scan_and_generate() {
    local dir_spec="$1" dir="${dir_spec%:*}" ext="${dir_spec#*:}"
    [[ ! -d "$dir" ]] && return
    for script in "$dir"/*; do
        [[ ! -f "$script" ]] && continue
        [[ "$script" == *.exe || "$script" == *.cmd || "$script" == *.ps1 ]] && continue
        local basename="${script##*/}" native="${script}.${ext}"
        __is_shell_script "$script" || continue
        [[ ! -f "$native" ]] && continue
        __should_exclude "$basename" && continue
        __generate_wrapper "$basename" "$ext"
        echo "$basename:$ext"
    done
}

__generate_all_wrappers() {
    local cache_valid=false
    if [[ -f "$__WORKAROUND_CACHE" ]]; then
        local cache_mtime=$(stat -c %Y "$__WORKAROUND_CACHE" 2>/dev/null || echo 0)
        local cache_age=$(($(date +%s) - cache_mtime))
        [[ $cache_age -lt $__WORKAROUND_CACHE_TTL ]] && cache_valid=true
    fi
    if $cache_valid; then
        while IFS=: read -r cmd ext; do
            [[ -n "$cmd" && -n "$ext" ]] && __generate_wrapper "$cmd" "$ext"
        done < "$__WORKAROUND_CACHE"
    else
        mkdir -p "$(dirname "$__WORKAROUND_CACHE")"
        : > "$__WORKAROUND_CACHE"
        for dir_spec in "${__SCAN_DIRS[@]}"; do
            __scan_and_generate "$dir_spec" >> "$__WORKAROUND_CACHE"
        done
    fi
}

__generate_all_wrappers

# Utility: refresh after installing new tools
mingw64-refresh-wrappers() {
    rm -f "$__WORKAROUND_CACHE"
    unset __MINGW64_WORKAROUNDS_LOADED
    source "$BASH_ENV"
    echo "Detected commands:"
    cat "$__WORKAROUND_CACHE" 2>/dev/null | cut -d: -f1 | sort | column
}
export -f mingw64-refresh-wrappers 2>/dev/null

2. Add to Claude Code settings (~/.claude/settings.json):

{
  "env": {
    "BASH_ENV": "C:/Users/YOUR_USERNAME/.claude/shell-fixes/mingw64-workarounds.sh"
  }
}

Detected commands (example)

On my system, this automatically detected 35 commands:

  • Docker: docker, docker-compose
  • npm globals: npm, npx, pnpm, yarn, tsc, ts-node, eslint, firebase, expo, etc.

Maintenance

  • After installing new tools: Run mingw64-refresh-wrappers to rescan
  • To add custom directories: Edit __SCAN_DIRS array
  • Cache auto-refreshes every hour
btull89 · 5 months ago

I'm also having this issue.

codemile · 5 months ago

@Daichi-Kudo thank you for sharing this script.

I had to modify your script to fix an escape error to replace "'s|\|/|g'" with "'s|\\|/|g'". I'm not sure why, but maybe it was related to the markdown editor on GitHub.

__to_unix_path() {
    echo "$1" | sed \
        -e 's|\\|/|g' \
        -e 's|^\([A-Za-z]\):|/\L\1|'
}

I hope they make an official fix, because you don't really notice this bug right away (especially if you're a new user to Claude), or alternatively it just confuses new users on Windows who give up trying to learn Claude.

caozhiyuan · 5 months ago

set env
"SHELLOPTS": "braceexpand"
in claude code settings.json

There are too many bugs in the latest version, especially when using Claude Code in Windows environments. It is recommended to use version 2.1.7 @Daichi-Kudo

codemile · 5 months ago

How can we get Anthropic's attention on this? I fear they don't know that all Windows users are impacted by this.

codemile · 5 months ago

My workaround was to switch to WSL (Windows Subsystem Linux) with Ubuntu to run Claude Code, and use the "WSL Extension" within Visual Code to connect to "WSL:Ubuntu" which allows you to run the WSL terminal inside Visual Code and Claude was able to connect to the IDE installed Claude extension. Which is pretty impressive that everything seems to be working.

If you're using Docker, there is a WSL integration feature on Docker Desktop for Windows to enable WSL support. I haven't tried it yet, but some of my projects require it, but I'm hopeful it'll work.

I was able to run the latest Claude Code and test output from Bash commands from a WSL terminal in Visual Code.

<img width="1749" height="637" alt="Image" src="https://github.com/user-attachments/assets/71d65dfb-0016-4cff-a871-691af035018a" />

caozhiyuan · 5 months ago

@codemile set env
"SHELLOPTS": "braceexpand"
in claude code settings.json this can't work?

codemile · 5 months ago

@caozhiyuan it was already enabled and didn't work.

caozhiyuan · 5 months ago

@codemile in env ?

  "env": {
	"CLAUDE_CODE_ATTRIBUTION_HEADER": "0",
	"SHELLOPTS": "braceexpand",
	"CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION": "false"
  },
codemile · 5 months ago

Nope, setting the env doesn't work.

<img width="541" height="272" alt="Image" src="https://github.com/user-attachments/assets/a25482be-a7f4-41d9-a665-a38460fc204c" />

caozhiyuan · 5 months ago

@codemile
check you settings.json
<img width="1021" height="491" alt="Image" src="https://github.com/user-attachments/assets/22b05250-bff2-4db6-901e-33abbf68dd72" />

@codemile in env ? `` "env": { "CLAUDE_CODE_ATTRIBUTION_HEADER": "0", "SHELLOPTS": "braceexpand", "CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION": "false" }, ``
caozhiyuan · 5 months ago

@codemile In claude cli.js, you can see the setting of the SHELLOPTS attribute, but gitbash is incompatible, causing the issue. As long as SHELLOPTS is not empty, the new attribute will not be applied, so there won't be any problem.

<img width="820" height="167" alt="Image" src="https://github.com/user-attachments/assets/ae70954d-90d4-4d5d-bd0f-c5dccfb1cb0c" />

caozhiyuan · 5 months ago

There are too many bugs in the latest version, especially when using Claude Code in Windows environments. It is recommended to use version 2.1.7 @codemile

orlinkata · 5 months ago

@codemile I have implemented the second workaround: https://github.com/anthropics/claude-code/issues/18748#issuecomment-3762646331

My build, run and test procedures are all in bash scripts and Claude code was not able to execute them. After applying the workaround now it works fine. I think that one more thing had to be applied - use 'slource script.sh' instead ./script.sh to call inside Claude. And there was a minor fix I can't recall now that has to be applied to the scripts related to BASHSOURCE. I will provide more details when I'm on my desktop

codemile · 5 months ago

@orlinkata that fix gives you the wrong impression that you've fixed the problem, but it only works for a few commands that it fixes. I had hooks that were failing because of (no content), and other stuff. It's not worth risking it, because you need to baby sit the Claude runs to see if it's getting (no content).

What makes this bug so deadly is that Claude assumes (no content) means success. That will get you at some point.

@caozhiyuan nope, none of those env settings work. I've been using WSL for awhile now and it seems stable.

caozhiyuan · 5 months ago
@codemile check you settings.json <img width="1021" height="491" alt="Image" src="https://github.com/user-attachments/assets/22b05250-bff2-4db6-901e-33abbf68dd72" /> > @codemile in env ? > > `` > "env": { > "CLAUDE_CODE_ATTRIBUTION_HEADER": "0", > "SHELLOPTS": "braceexpand", > "CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION": "false" > }, > ``

@codemile I added the environment variables above to my settings.json file without any problems, as you can see in the screenshot above. Could you please share a screenshot of your settings.json file?

gatusmart · 5 months ago

Same issue for az commands

codemile · 5 months ago

@caozhiyuan it's weird how it's working for you. I'm jealous.

<img width="676" height="344" alt="Image" src="https://github.com/user-attachments/assets/a0692376-9836-4cae-8ad2-aaf585bf6023" />

gatusmart · 5 months ago
## Updated: Dynamic Workaround Solution (v2) The previous static workaround required manual updates. Here's an improved dynamic detection approach that automatically handles all affected commands. ### How it works 1. Scans common tool directories (npm global, Docker, etc.) 2. Detects shell script wrappers that have .exe/.cmd equivalents 3. Auto-generates wrapper functions at shell startup 4. Caches results for 1 hour to avoid repeated scans 5. Provides utility commands for maintenance ### Setup 1. Create the workaround script: Save to ~/.claude/shell-fixes/mingw64-workarounds.sh: #!/bin/bash # Claude Code MINGW64 Workarounds - Dynamic Detection # Issue: https://github.com/anthropics/claude-code/issues/18748 [[ "$MSYSTEM" != MINGW && "$MSYSTEM" != MSYS ]] && return 0 [[ -n "$__MINGW64_WORKAROUNDS_LOADED" ]] && return 0 export __MINGW64_WORKAROUNDS_LOADED=1 __to_unix_path() { echo "$1" | sed -e 's|\|/|g' -e 's|^\([A-Za-z]\):|/\L\1|' } __HOME="$(__to_unix_path "$USERPROFILE")" [[ -z "$__HOME" ]] && __HOME="$HOME" __WORKAROUND_CACHE="$__HOME/.claude/shell-fixes/.cache" __WORKAROUND_CACHE_TTL=3600 __APPDATA_UNIX="$(__to_unix_path "$APPDATA")" __LOCALAPPDATA_UNIX="$(__to_unix_path "$LOCALAPPDATA")" __SCAN_DIRS=( "/c/Program Files/Docker/Docker/resources/bin:exe" "${__APPDATA_UNIX}/npm:cmd" "${__LOCALAPPDATA_UNIX}/pnpm:cmd" "${__HOME}/.local/bin:exe" "/c/ProgramData/chocolatey/bin:exe" ) __EXCLUDE_COMMANDS=( "sh" "bash" "env" "cat" "ls" "cp" "mv" "rm" "mkdir" "grep" "sed" "awk" "find" "which" "where" "cmd" "powershell" "pwsh" ) __should_exclude() { local cmd="$1" for excl in "${__EXCLUDE_COMMANDS[@]}"; do [[ "$cmd" == "$excl" ]] && return 0 done return 1 } __is_shell_script() { local file="$1" [[ ! -f "$file" ]] && return 1 local first_bytes=$(head -c 2 "$file" 2>/dev/null) [[ "$first_bytes" == "#!" ]] && return 0 return 1 } __generate_wrapper() { local cmd="$1" ext="$2" eval "${cmd}() { ${cmd}.${ext} \"\$@\"; }" export -f "$cmd" 2>/dev/null } __scan_and_generate() { local dir_spec="$1" dir="${dir_spec%:}" ext="${dir_spec#:}" [[ ! -d "$dir" ]] && return for script in "$dir"/; do [[ ! -f "$script" ]] && continue [[ "$script" == .exe || "$script" == .cmd || "$script" == .ps1 ]] && continue local basename="${script##/}" native="${script}.${ext}" __is_shell_script "$script" || continue [[ ! -f "$native" ]] && continue __should_exclude "$basename" && continue __generate_wrapper "$basename" "$ext" echo "$basename:$ext" done } __generate_all_wrappers() { local cache_valid=false if [[ -f "$__WORKAROUND_CACHE" ]]; then local cache_mtime=$(stat -c %Y "$__WORKAROUND_CACHE" 2>/dev/null || echo 0) local cache_age=$(($(date +%s) - cache_mtime)) [[ $cache_age -lt $__WORKAROUND_CACHE_TTL ]] && cache_valid=true fi if $cache_valid; then while IFS=: read -r cmd ext; do [[ -n "$cmd" && -n "$ext" ]] && __generate_wrapper "$cmd" "$ext" done < "$__WORKAROUND_CACHE" else mkdir -p "$(dirname "$__WORKAROUND_CACHE")" : > "$__WORKAROUND_CACHE" for dir_spec in "${__SCAN_DIRS[@]}"; do __scan_and_generate "$dir_spec" >> "$__WORKAROUND_CACHE" done fi } __generate_all_wrappers # Utility: refresh after installing new tools mingw64-refresh-wrappers() { rm -f "$__WORKAROUND_CACHE" unset __MINGW64_WORKAROUNDS_LOADED source "$BASH_ENV" echo "Detected commands:" cat "$__WORKAROUND_CACHE" 2>/dev/null | cut -d: -f1 | sort | column } export -f mingw64-refresh-wrappers 2>/dev/null 2. Add to Claude Code settings (~/.claude/settings.json): { "env": { "BASH_ENV": "C:/Users/YOUR_USERNAME/.claude/shell-fixes/mingw64-workarounds.sh" } } ### Detected commands (example) On my system, this automatically detected 35 commands: Docker: docker, docker-compose npm globals: npm, npx, pnpm, yarn, tsc, ts-node, eslint, firebase, expo, etc. ### Maintenance After installing new tools: Run mingw64-refresh-wrappers to rescan To add custom directories: Edit __SCAN_DIRS array Cache auto-refreshes every hour

I had to use this and added az commands folder to the scan dirs. And had to change __to_unix_path() a little bit cause it was causing some errors

codemile · 5 months ago

@caozhiyuan I downgraded Claude to 2.1.7 but still have the problem. I thought that version was supposed to work. I feel like this bug is gaslighting me, because I thought this worked a few weeks ago and was a new bug introduced after 2.1.7.

<img width="958" height="624" alt="Image" src="https://github.com/user-attachments/assets/4e3e3476-65c8-4088-a004-b55fbc6d6148" />

caozhiyuan · 5 months ago

@codemile your claude code use git bash or ? default shell is git bash. and should output
$ echo $SHELLOPTS
braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor

caozhiyuan · 5 months ago

@codemile echo $CLAUDE_CODE_GIT_BASH_PATH

caozhiyuan · 5 months ago

install latest git windows https://gitforwindows.org/ @codemile

codemile · 5 months ago

@caozhiyuan thanks for your help! It's working now with 2.1.7

To get it to work, I had to uninstall Claude from the System Settings and then delete my ~/.claude folder, then I reinstalled Claude and switched to the stable channel. Seems it was a cache related issue, but deleting ~/.claude/plugins/cache wasn't enough. So I did a full reinstall which worked.

I did a test with 2.1.20 but it's still broken but switching back to 2.1.7 works.

<img width="535" height="398" alt="Image" src="https://github.com/user-attachments/assets/084a221c-6ce4-44b1-90e9-c0cdc47ea1a0" />

codemile · 5 months ago

It seems this bug was introduced in 2.1.9

When reviewing the 2.1.9 release note, the only thing related to the terminal is this change, which doesn't sound related but unless they made undocumented changes, then I guess it must be the source of the bug

Fixed Ctrl+Z suspend not working in terminals using Kitty keyboard protocol (Ghostty, iTerm2, kitty, WezTerm)

<img width="927" height="701" alt="Image" src="https://github.com/user-attachments/assets/895f8566-1ac2-420c-9fca-b7aa9a2fd20e" />

thundo · 5 months ago

Summary

When Node.js spawn() executes a shell script file via bash on Windows MINGW64 (Git Bash), stdout is not captured. The file descriptors are not properly inherited when bash forks a subprocess to execute the script.

Evidence

| Command Pattern | stdout Captured? |
|----------------|-----------------|
| spawn("bash", ["-c", "echo hello"]) | ✅ Yes |
| spawn("bash", ["-c", "node -e '...'"]) | ✅ Yes |
| spawn("bash", ["-c", ". script.sh"]) | ✅ Yes (sourced) |
| spawn("bash", ["-c", "source script.sh"]) | ✅ Yes (sourced) |
| spawn("bash", ["-c", "script.sh"]) | ❌ No |
| spawn("bash", ["script.sh"]) | ❌ No |
| spawn("bash", ["-c", "(script.sh)"]) | ❌ No (subshell) |
| spawn("python", ["script.py"]) | ✅ Yes |

Key Finding

The issue occurs only when bash forks a subprocess to execute a shell script file. When the script is sourced (runs in the same shell process), stdout is captured correctly.

bash -c ". script.sh"      → stdout is connected to pipe:[0] ✅
bash -c "script.sh"        → stdout is disconnected ❌
bash script.sh             → stdout is disconnected ❌

Root Cause

This is a file descriptor inheritance bug in Git Bash (MINGW64) when used with Node.js:

  1. Node.js creates a child process with spawn("bash", ...) and sets up pipes for stdio
  2. When bash receives an inline command (-c "echo hello"), it executes in the current process and stdout flows correctly
  3. When bash receives a script file argument, it forks a new bash subprocess to execute the script
  4. On MINGW64, this forked subprocess does not properly inherit the stdout file descriptor that Node.js is reading from
  5. The script runs successfully (exit code 0) but its output goes nowhere

Why npm/pnpm Are Affected

On Git Bash, npm resolves to a shell script wrapper at /c/Users/.../npm which internally runs node npm-cli.js. When Claude Code runs:

spawn("bash", ["-c", "npm --version"])

Bash finds and executes the npm shell script file, triggering the fd inheritance bug.

Why npm.cmd Works

npm.cmd is a Windows batch file, not a shell script. When run via:

spawn("bash", ["-c", "npm.cmd --version"])

Bash delegates to Windows to run the .cmd file, which doesn't trigger the bash fork behavior.

Workarounds

1. Source scripts instead of executing them

// Instead of:
spawn("bash", ["-c", "npm --version"])

// Use:
spawn("bash", ["-c", "source $(which npm) --version"])
// Or:
spawn("bash", ["-c", ". $(which npm) --version"])

2. Use .cmd versions on Windows

// Detect MINGW64 and use .cmd suffix
if (process.env.MSYSTEM?.startsWith('MINGW')) {
  command = command + '.cmd';
}

3. Inline the command instead of using script files

// Instead of executing npm shell script:
spawn("bash", ["-c", "npm --version"])

// Execute node directly:
spawn("node", ["/path/to/npm-cli.js", "--version"])

Proposed Fix

In the command resolution layer, detect MINGW64 and resolve shell script wrappers to their .cmd equivalents:

function resolveCommand(cmd) {
  const isMINGW = process.env.MSYSTEM?.startsWith('MINGW') ||
                  process.env.MSYSTEM?.startsWith('MSYS');

  if (!isMINGW) return cmd;

  // On MINGW64, prefer .cmd versions to avoid shell script fd inheritance bug
  const extensions = ['.cmd', '.exe', '.bat'];
  for (const ext of extensions) {
    const resolved = findInPath(cmd + ext);
    if (resolved) return resolved;
  }

  return cmd;
}

Trivial Workaround

This is obviously very suboptimal, but works well in practice.

# CLAUDE.md
...
...
## No Content on Bash output
On Claude >2.1.7 you may be a victim of "Bash tool returns empty output when executing shell script files on Windows (MINGW64)" https://github.com/anthropics/claude-code/issues/18748.
The bug is in Claude Code's subprocess stdout capture mechanism. When a shell script wrapper spawns a child process on MINGW64:

Claude Code → bash → npm (shell script) → node.exe (subprocess)
                                          ↑
                              stdout lost here

The output from the nested subprocess doesn't propagate back through the shell script layer to Claude Code's capture mechanism.

### Workaround
Use .cmd or .exe suffixes (npm.cmd, pnpm.cmd, node.exe)
caozhiyuan · 5 months ago

I've uninstalled it and switched to opencode. Downgrading claude code to 2.1.7, npm --version still doesn't work, although it worked before. Who knows what this software is doing in the background.

gatusmart · 5 months ago

The shellwrapperfix causes issues sometimes. Some commands wont run correctly you will get issues like: c/Program could not be found

probably some escaping that is happening incorrectly

btull89 · 5 months ago

The vs code extension appears to work when the CLI does not.

andrewLapshov · 5 months ago

2.1.31, works well

<img width="582" height="559" alt="Image" src="https://github.com/user-attachments/assets/e4648570-b982-4ae6-8518-6f779d128f5d" />

Daichi-Kudo · 4 months ago

Update: Git for Windows upgrade + MSYS2 Stdout Bridge workaround (2026-02-18)

Finding 1: Git for Windows upgrade significantly improves the situation

Upgrading Git for Windows from 2.34.1 (2021) to 2.53.0 resolved stdout capture for Windows native .exe processes:

| Component | Before | After |
|-----------|--------|-------|
| Git | 2.34.1 | 2.53.0 |
| Bash | 4.4.23 | 5.2.37 |
| MSYS2 runtime | 3.1.7 (2021-10) | 3.6.6 (2026-01) |

After upgrade, commands like git, docker, npm, node, python, poetry, gcloud, firebase, gh, az, etc. all produce captured stdout correctly. However, bash builtins (echo, pwd, printf) and MSYS2 utilities (ls, cat, grep, head, uname, etc.) still lose stdout.

Finding 2: Root cause clarification

The issue is specifically that Claude Code's pipe captures stdout from Windows native .exe processes but not from MSYS2 binaries or bash builtins, which use a different I/O layer (msys-2.0.dll).

Evidence:

  • echo hello > /dev/null → exit 0 (shell works, just stdout pipe is broken)
  • echo hello > /tmp/file.txt → file created with content (file I/O works)
  • cmd.exe /c echo hello → "hello" captured correctly (Windows native I/O works)
  • echo hello → exit 1, no output (MSYS2 I/O → Claude Code pipe broken)

Finding 3: MSYS2 Stdout Bridge workaround

We developed a workaround that wraps MSYS2 commands to redirect their output through cmd.exe /c type (Windows native), making it visible to Claude Code:

__msys2_stdout_wrap() {
    local __t="${TEMP:-/tmp}/__mw_$$_${RANDOM}"
    "$@" > "$__t" 2>&1
    local __e=$?
    cmd.exe /c type "$(cygpath -w "$__t")" 2>/dev/null
    rm -f "$__t" 2>/dev/null
    return $__e
}

# Wrap bash builtins
echo()  { __msys2_stdout_wrap builtin echo "$@"; }
printf(){ __msys2_stdout_wrap builtin printf "$@"; }
pwd()   { __msys2_stdout_wrap builtin pwd "$@"; }

# Wrap MSYS2 utilities
ls()   { __msys2_stdout_wrap /usr/bin/ls "$@"; }
cat()  { __msys2_stdout_wrap /usr/bin/cat "$@"; }
grep() { __msys2_stdout_wrap /usr/bin/grep "$@"; }
# ... etc for head, tail, sed, awk, find, sort, wc, etc.

Set via BASH_ENV in Claude Code's settings.json:

{
  "env": {
    "BASH_ENV": "/path/to/mingw64-workarounds.sh"
  }
}

Result

| Category | Before | After workaround |
|----------|--------|-----------------|
| Windows .exe (git, docker, npm, etc.) | Works (after Git upgrade) | Works |
| Bash builtins (echo, pwd, printf) | Broken | Works |
| MSYS2 utilities (ls, cat, grep, etc.) | Broken | Works |
| Pipes & chains (echo x \| grep x) | Broken | Works |

Full workaround script: https://gist.github.com/ (will publish separately)

Recommendation

  1. Windows users: Update Git for Windows to latest (2.53+) first
  2. Claude Code team: The native fix should ensure MSYS2 I/O layer stdout is captured, not just Windows native .exe stdout
gatusmart · 4 months ago

Is this issue fixed or is the issue still there? I had the issue still on Claude Code v2.1.47. updated to Git for Windows 2.53.0 . Had to revert back to using the mingw64-workarounds.sh fix

Humancell · 4 months ago

It would be really great if this could be fixed. It is wasting so much time (and tokens!) and making the entire development process a pain. Yes ... there are work arounds ... but c'mon ... just fix the bug, please?

MarcusJellinghaus · 4 months ago

Comment for <https://github.com/anthropics/claude-code/issues/18748>

---

We've been investigating this extensively on Windows 10 with Git for Windows 2.53.0.

What works: Downgrading to Claude Code 2.1.40 fully resolves the issue — CLI tool stdout is captured correctly without any workarounds. No BASH_ENV tricks needed.

What we tried that did NOT help:

  • BASH_ENV function redefining the command to call Python directly — claude.exe corrupts the parent process stdout pipe, so even output flushed before main() disappears
  • CREATE_NO_WINDOW on subprocess Popen calls — causes ~150s pipe hang on Git 2.53.0
  • .bat file wrapper — cmd.exe output not captured

We're now pinned to 2.1.40 with DISABLE_AUTOUPDATER=1.

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

gatusmart · 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: #35761

Why? Doesn’t it cause Claude to need to write commands differently?

Erdou · 3 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: #35761
/plugin marketplace add cruzlauroiii/claude-code
  ⎿  Error: The name 'claude-code-plugins' is reserved for official Anthropic marketplaces. Only repositories from
     'github.com/anthropics/' can use this name.

Did I do something wrong?

Sid180603 · 2 months ago

[https://gist.github.com/Sid180603/276580f6c006e2ad2b92a76f04c2f415](url)

### Fix: WSL Bash stdout capture — native Windows wrapper (confirmed working)

Root cause

Claude Code's Bash tool spawns bash.exe and captures stdout via a Node.js pipe. On Windows with WSL2, bash.exe is the WSL launcher — commands execute inside the Linux VM, and stdout must cross the hypervisor boundary back to the Windows host process. This pipe silently drops data. Commands execute correctly, but Claude Code receives nothing → (Bash completed with no output).

This is not a buffering issue. LD_PRELOAD + stdbuf (forcing line-buffered stdout) does not fix it. The pipe itself is broken at the VM boundary.

Solution

A ~200-line C# Windows-native .exe that replaces bash.exe as CLAUDE_CODE_GIT_BASH_PATH. It bypasses the broken pipe entirely using file-based I/O:

Claude Code (Node.js) → bash.exe (wrapper) → wsl.exe → bash (writes to temp file on Windows FS)

wrapper reads temp file with File.ReadAllText()

Console.Write() → Node.js captures it ✓

How it works:

  1. Receives -c -l "command" from Claude Code (same interface as real bash)
  2. Writes a temp .sh script to %TEMP% with: exec > tempfile.out 2>&1
  3. Runs: wsl.exe -d <distro> -- bash -l script.sh

(does NOT capture wsl.exe stdout — that's the broken pipe)

  1. After WSL exits, reads the .out file using native Windows File.ReadAllText()
  2. Emits via Console.Write() — Node.js captures native .exe stdout perfectly
  3. Propagates exit code via a separate .rc temp file
  4. Cleans up all temp files (+ cleans stale files from crashed runs older than 10 min)

Important gotcha: -c -l flag handling

Claude Code sends: bash -c -l "shopt -u extglob ... && eval '...' ...".
The -l (login shell) flag comes after -c. A naive wrapper that takes args[1] as the command will get -l instead of the actual command string. The wrapper must skip flags after -c and pass them through to the bash invocation.

Source code

Full source (C#, .NET Framework 4.x — ships with Windows): WslBashWrapper.cs
<https://gist.github.com/Sid180603/276580f6c006e2ad2b92a76f04c2f415>

Setup

  1. Create directory

mkdir "$env:USERPROFILE\.claude\bin" -Force

  1. Save WslBashWrapper.cs to:

~/.claude/shell-fixes/WslBashWrapper.cs

  1. Compile (uses .NET Framework already on your machine — no install needed)

& "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe"
/nologo /optimize

/out:"$env:USERPROFILE\.claude\bin\bash.exe" `
"$env:USERPROFILE\.claude\shell-fixes\WslBashWrapper.cs"

  1. Add to ~/.claude/settings.json

{
"env": {
"CLAUDE_CODE_GIT_BASH_PATH": "C:\\Users\\<username>\\.claude\\bin\\bash.exe"
}
}

  1. (Optional) Set WSL distro if not Debian

Default is "Debian". Override with env var:
"CC_WSL_DISTRO": "Ubuntu-24.04"

  1. Restart Claude Code

What was tried and didn't work

  • LD_PRELOAD + libstdbuf.so (force line-buffered stdout)

Not a buffering issue — the pipe itself drops data

  • BASH_ENV script with __wsl_stdout_bridge (redirect to file, read via cmd.exe /c type)

cmd.exe output still goes through the broken pipe

  • Switching to Git Bash

Works, but loses native Docker/WSL tool access

Environment

  • Windows 11, WSL2 with Debian 13
  • Claude Code (CLI + VS Code extension)
  • Docker Desktop running inside WSL

Confirmed working for: echo, multiline output, docker ps, git status, whoami, pwd, complex piped commands with exit code propagation.

jchesshircr · 1 month ago