Windows: Git Bash shell-snapshot embeds raw Windows PATH unconverted, breaking short-name command resolution

Status Open
Maintainer reply None cached
Activity 6 comments · opened Jul 1, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Environment: Windows 11, Claude Code using Git Bash as the shell for the Bash tool.

Every Bash tool call resolves bare command names to "command not found" — ls, grep, rm, pwsh, python, python3, etc. all fail — while invoking the exact same binaries by absolute path (e.g. /c/Program Files/PowerShell/7/pwsh.exe) works fine.

Root cause: Claude Code generates a shell snapshot per process start at ~/.claude/shell-snapshots/snapshot-bash-<timestamp>-<id>.sh, which is sourced on every Bash tool invocation. This snapshot contains a line like:

export PATH='C:\Windows\system32;C:\Windows;...;C:\Program Files\PowerShell\7\;...:/c/Users/<user>/AppData/Roaming/Claude/local-agent-mode-sessions/.../bin'
i.e. the native Windows PATH (semicolon-separated, drive-letter paths like C:\...) is captured verbatim, without conversion to POSIX form, then some additional already-POSIX plugin bin directories are appended with :. Since bash's PATH parsing splits on :, and every Windows path segment contains a : right after its drive letter (C:\...), the entire PATH gets shredded into meaningless fragments — so name-based PATH lookup can never succeed for any entry, Windows or POSIX.

What does NOT fix it (all tested, no effect):

Setting a corrected POSIX PATH under env in settings.json (global ~/.claude/settings.json) — other new keys added under the same env (e.g. a custom BASH_ENV) do propagate correctly into the spawned bash process, confirmed by reading them back inside a Bash tool call — but PATH itself is always re-exported by the snapshot regardless.
Adding PATH-fix logic to ~/.bashrc / ~/.bash_profile.
Setting BASH_ENV to point at a repair script — confirmed the env var reaches the process, but the script is never sourced (no side effect from a debug marker written by the script).
Restarting the app (twice) and killing a lingering background process — a new snapshot file is generated each time, but it always re-captures the same broken raw Windows PATH.
Confirmed workaround (session-scoped only): manually editing the current snapshot's export PATH=... line to a cygpath -up-converted equivalent (using Git's own usr/bin/cygpath.exe), and prepending Git's usr/bin (coreutils aren't present in the native Windows PATH at all, so they're missing from the captured value too), immediately fixes command resolution for the rest of that process's lifetime.

What Should Happen?

When generating shell-snapshots/snapshot-bash-*.sh on Windows, Claude Code should run the captured PATH through a Windows→POSIX path-list conversion (equivalent to cygpath -up) before writing the export PATH=... line, and ensure Git's usr/bin (and mingw64/bin) are included even though they aren't part of the native Windows PATH. With that fix, short-name commands (ls, grep, pwsh, python, etc.) should resolve correctly in every new Bash tool session without any manual workaround.

Error Messages/Logs

Steps to Reproduce

On Windows, use Claude Code with Git Bash as the shell for the Bash tool (default on Windows installs with Git for Windows present).
Start a Claude Code session and run any short-name command through the Bash tool that isn't a shell builtin, e.g.:
ls
pwsh -Command "echo ok"
python --version
grep --version
Observe the result: bash: ls: command not found, bash: pwsh: command not found, etc., even though these binaries are installed and present in the user's Windows PATH.
Confirm the binaries do work when invoked by absolute path, e.g.:
/c/Program Files/PowerShell/7/pwsh.exe -Command "echo ok"
This succeeds, proving the binaries and permissions are fine — only name-based PATH resolution fails.
Inspect ~/.claude/shell-snapshots/ and open the most recently modified file matching snapshot-bash-*.sh. Find the line starting with export PATH='...'. Note that its value is the raw, semicolon-separated native Windows PATH (e.g. C:\Windows\system32;C:\Windows;...;C:\Program Files\PowerShell\7\;...), not a POSIX colon-separated form.
(Optional, to rule out a settings.json misconfiguration) Add a new custom key, e.g. "TEST_VAR": "hello", under env in ~/.claude/settings.json, restart, and run echo $TEST_VAR via the Bash tool — it correctly prints hello, showing that env overrides do propagate to the Bash tool's process. Now try setting "PATH" to a corrected POSIX value the same way — restart and repeat step 5: the snapshot's export PATH=... line still contains the raw unconverted Windows value, unaffected by the env.PATH setting.
Restart the Claude Code application and repeat step 2. A brand-new file appears in ~/.claude/shell-snapshots/ with a new timestamp, but it again contains the same unconverted, broken PATH — the bug reproduces on every fresh session/process start.

Claude Model

None

Is this a regression?

No, this never worked

Last Working Version

_No response_

Claude Code Version

Claude 1.17377.1 (2b3ab4) 2026-06-30T20:11:42.000Z

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

PowerShell

Additional Information

_No response_

View original on GitHub ↗

4 Comments

Brumaire · 3 months ago

Confirming this is still reproducible on Claude Code 2.1.206 (2026-07-16), Windows 11, Git Bash. The snapshot generated for today's session still contains the raw Windows-format PATH:

export PATH='C:\Program Files\...;C:\Windows\system32;C:\Windows;...'

semicolon-separated, drive-letter paths — bash splits on : and every lookup fails, exactly as described in the OP.

Why startup-time fixes don't work

The Bash tool wraps every command roughly as:

bash -c "source <snapshot> && shopt -u extglob && eval '<user command>' && ..."

So even when a repair runs at bash startup (via BASH_ENV, .bashrc, or env in settings.json), the snapshot's export PATH='C:\...' is sourced afterwards as part of the wrapped command and clobbers it. The repair has to run after the snapshot is sourced — i.e., inside the user command itself.

Working workaround: PreToolUse hook that prepends a self-heal prefix

This has been running reliably for ~3 months on my machine. Two pieces:

1. Set a user-level env var pointing at Git's own profile (which rebuilds PATH in POSIX form and adds /usr/bin, /mingw64/bin):

[Environment]::SetEnvironmentVariable("BASH_ENV", "C:\Program Files\Git\etc\profile", "User")

(BASH_ENV isn't used for its normal bash semantics here — it's just a convenient, already-propagated variable carrying the profile path into the session.)

2. Global PreToolUse hook in ~/.claude/settings.json:

"hooks": {
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "& 'C:/Users/<user>/.claude/hooks/fix-bash-path.ps1'",
          "shell": "powershell",
          "timeout": 10
        }
      ]
    }
  ]
}

with ~/.claude/hooks/fix-bash-path.ps1:

# Prepend self-heal prefix to every Bash tool command so Git Bash on Windows
# can find external commands (git, head, etc.) even when Claude Code's shell
# snapshot ships a Windows-format PATH that bash cannot parse.
#
# Self-heal logic injected into the target bash session:
#   if $PATH already contains /usr/bin -> snapshot is fine, do nothing
#   elif $BASH_ENV is unset            -> not Git Bash on Windows, do nothing
#   else                                -> source $BASH_ENV (= /etc/profile)
#                                          which translates PATH to Unix form
#
# Result: when Claude Code eventually fixes the snapshot upstream, /usr/bin
# will already be in PATH and this hook becomes a silent no-op automatically.

[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$raw = [Console]::In.ReadToEnd()
if (-not $raw) { exit 0 }

try {
    $j = $raw | ConvertFrom-Json
} catch {
    exit 0
}

$cmd = $j.tool_input.command
if (-not $cmd) { exit 0 }

$prefix = '[[ ":$PATH:" == *":/usr/bin:"* ]] || [[ -z "$BASH_ENV" ]] || source "$BASH_ENV" 2>/dev/null; '

# Carry forward every field of tool_input so we don't drop description / timeout / etc.
$updatedInput = @{}
foreach ($prop in $j.tool_input.PSObject.Properties) {
    $updatedInput[$prop.Name] = $prop.Value
}
$updatedInput['command'] = $prefix + $cmd

$out = @{
    hookSpecificOutput = @{
        hookEventName = 'PreToolUse'
        updatedInput  = $updatedInput
    }
}

$out | ConvertTo-Json -Depth 10 -Compress

The injected prefix is a three-way short-circuit, so it's safe to leave in place permanently:

  • PATH already contains /usr/bin → snapshot was fine (or upstream fixed it) → no-op
  • BASH_ENV unset → not Git Bash on Windows (Linux/macOS) → no-op
  • otherwise → source Git's /etc/profile, which converts PATH to POSIX form

With this in place all external commands (git, coreutils, pwsh, python, node, …) resolve normally in every Bash tool call, and once the snapshot generator is fixed upstream the hook retires itself automatically.

cderv · 1 month ago

I hit this too on Windows 11, Claude Code with Git Bash, CLAUDE_CODE_GIT_BASH_PATH set plus CLAUDE_CODE_USE_POWERSHELL_TOOL=1 in settings.json.

Symptom matched the OP exactly, down to which itself coming back command not found on line 1, consistent with the whole PATH shredding into one bogus fragment rather than just specific tools being missing.

One data point that might help narrow it down: I had two Claude Code sessions open on the same machine, started only minutes apart, same settings. One hit the bug, the other's shell-snapshot had a correctly POSIX-translated PATH (/c/Users/.../bin:/mingw64/bin:/usr/bin:...). So whatever breaks the conversion looks timing-dependent at snapshot-generation time, not a static per-machine misconfiguration. Not sure if CLAUDE_CODE_USE_POWERSHELL_TOOL is relevant here or a red herring, both affected sessions had it set.

Didn't try the PreToolUse hook workaround above yet, worked around it for now by switching that session to the PowerShell tool.

Klinkenbecker · 1 month ago

Thanks for the pointers, but neither #72728 nor #50541 is this issue. They are adjacent — all Windows, all area:bash, all with "PATH" in them — but they are different defects at different stages, and fixing either would leave this one exactly as broken.

The one-line test: in this issue, nothing fails.

Every command resolves. No command not found, no syntax errors, no missing binaries. The shell simply is not the one I configured, and it is 10.5 ms per call slower than the one I asked for. Both suggested duplicates are about a shell misbehaving; this is about the wrong binary being spawned in the first place.

| | #72728 | #50541 | this issue |
|---|---|---|---|
| What goes wrong | Correct shell runs, malformed PATH inside it | Correct shell runs, PATH injected unquoted | Wrong shell binary is selected |
| Symptom | ls: command not found | `syntax error near unexpected token (' ` | None. Everything works. |
| Cause | Windows-form PATH not converted to POSIX; bash splits on drive-letter colons | Parentheses in PATH parsed as subshell syntax |
CLAUDE_CODE_GIT_BASH_PATH never consulted; PATH discovery wins |
| Fix goes in | Snapshot generation (convert the PATH list) | PATH injection (quote it) | Shell selection (consult the configured path first) |
| Observable |
export PATH= line in the snapshot | The failing command line | ExecutablePath of the spawned bash.exe` |

Checked against my machine: the snapshot's PATH is well-formed, ls / grep / python all resolve, and no command produces a parse error. #72728's and #50541's entire symptoms are absent here. What is wrong is which binary is running them.

The ordering argument settles it. Claude Code must choose which bash.exe to spawn before it can generate a snapshot for it (#72728) or inject a PATH into it (#50541). This defect is strictly upstream of both. Fix them tomorrow and CLAUDE_CODE_GIT_BASH_PATH still loses to PATH discovery.

I suspect these matched because this report says "PATH" frequently — PATH discovery is the mechanism by which the wrong shell gets chosen. But PATH's contents, format and quoting are all fine here. The defect is that PATH is consulted at all, ahead of a configuration variable whose only purpose is to override it.

---

One thing that genuinely is relevant to #72728, and I have posted it there rather than here: I hit the mirror image of that defect. A snapshot generated by Git Bash leaves MSYS-rooted entries (/mingw64/bin, /usr/bin, /bin) in PATH, which a Windows-native shell cannot resolve — after sourcing it, git and perl both become NOT FOUND, verified with a control showing both resolving beforehand. So the snapshot's PATH is not normalised to the consuming shell's convention in either direction: Windows-form into a POSIX shell shreds on colons (#72728), POSIX-form into a Windows-native shell fails to resolve. One underlying defect, two faces. Still not this issue.

Klinkenbecker · 1 month ago

Confirming this from the opposite direction, and flagging one thing about the proposed fix.

The same defect, mirrored

You hit Windows-form PATH written into a POSIX shell, where bash splits on the drive-letter colons and nothing resolves. I hit the reverse: POSIX-form PATH written into a Windows-native shell, where the entries are not valid Windows paths and nothing resolves.

My shell is a native Windows bash implementation (not MSYS), so it needs Windows-rooted paths. After sourcing a Git-Bash-generated snapshot, its PATH contains /mingw64/bin, /usr/local/bin, /usr/bin and /bin verbatim. Those are MSYS mount points, meaningless to a native Windows process, so the binaries in them become unreachable:

BEFORE sourcing the snapshot:
  git    -> F:\Tools\PortableGit\mingw64\bin\git.EXE
  perl   -> F:\Tools\PortableGit\usr\bin\perl.EXE
  cmd    -> C:\WINDOWS\System32\cmd.EXE

AFTER sourcing the same snapshot:
  git    -> NOT-FOUND
  perl   -> NOT-FOUND
  cmd    -> C:/WINDOWS/System32\cmd.EXE

The control is the "before" block: both binaries resolve fine until the snapshot is sourced. git stops working purely because the snapshot's PATH replaced usable entries with MSYS-rooted ones.

So the underlying defect is broader than one direction

The snapshot's PATH is not normalised to the convention of the shell that will consume it. That produces two different failures depending on which way the mismatch runs:

| Snapshot PATH form | Consuming shell | Result |
|---|---|---|
| Windows (C:\...;C:\...) | MSYS bash | Splits on drive-letter colons, shreds (this issue) |
| POSIX (/mingw64/bin:...) | Windows-native bash | Entries are not real paths, nothing resolves |

Same root cause, opposite face.

Why the proposed fix needs a condition

The fix suggested above is to run the captured PATH through cygpath -up so it is always POSIX form. That fixes the MSYS case completely — and makes the Windows-native case worse, because it guarantees every entry is in the form that shell cannot use.

The snapshot is generated for a specific shell, so the conversion has to be conditioned on which shell that is:

  • MSYS/Cygwin bash (Git Bash, MSYS2) — convert to POSIX, as proposed, and add Git's usr/bin and mingw64/bin since they are absent from the native Windows PATH.
  • A Windows-native shell — leave PATH in Windows form, or convert to it.

An unconditional cygpath -up would trade one broken configuration for another.

Incidental, possibly useful

Two things I found while chasing this, both of which corrupt the snapshot regardless of path form:

  1. Unguarded output in ~/.bashrc / ~/.bash_profile lands inside the snapshot's export PATH=. Mine contained a debug echo, and the literal text rc-loaded .bash_profile ended up as the first element of PATH in the generated snapshot. It appears in the Git Bash snapshot on this machine. Anything an rc file prints during snapshot generation is captured as data. Guarding those lines with case $- in *i*) fixed it.
  2. Stale drive mappings make the System32 WSL stub write to stderr on every invocation — five lines of wsl: Failed to translate 'X:\...' here — which contaminates captured output the same way.

Environment: Claude Code 2.1.270, Windows 10 Pro 19045 x64.

Showing cached comments. Read the full discussion on GitHub ↗