[BUG] Windows/Git Bash: shell snapshot silently writes the raw Windows PATH when the PATH probe times out, so ls/grep/date are "command not found" for the whole session

Status Open
Reported on v2.1.286
Maintainer reply None cached
Activity 3 comments · opened Oct 3, 2026

Environment

  • Claude Code 2.1.286 (desktop app, Code tab), Windows 11, Git for Windows bash
  • The machine is often under heavy CPU load when sessions start

What happens

Some sessions start with a broken Bash tool: every command fails with command not found for ls, grep, date, cut, head, tr. The newest file in ~/.claude/shell-snapshots/ then contains a Windows-format, ;-separated PATH, which bash cannot use:

export PATH='C:\WINDOWS\system32;C:\WINDOWS;...'

Snapshots from other sessions on the same binary contain the expected POSIX PATH (/c/Users/<user>/bin:/mingw64/bin:/usr/local/bin:/usr/bin:/bin:...). The rest of the snapshot is identical; only the export PATH= line differs. The session never recovers, because every Bash call sources that snapshot.

Why (from the bundled code of 2.1.286)

Before writing the snapshot, Claude Code runs bash -lc 'echo "$PATH"' with a 10 s timeout and reject: false. Only when that probe exits 0 with output is its PATH used; otherwise the code silently keeps process.env.PATH (the raw Windows PATH) and writes it into the snapshot unconverted. The extra entries appended afterwards are converted to POSIX; the base PATH is not.

Evidence

154 snapshots on this machine (2026-09-03 to 2026-10-02):

  • 12 have the Windows-format PATH; 142 are POSIX.
  • Time between the timestamp in the snapshot file name (taken before the probe) and the file creation time: all 12 bad snapshots took 13.4 s to 299 s; the median of the good ones is 2.2 s. No bad snapshot was created quickly.
  • With an idle CPU, the same probe takes about 0.6 s and returns a POSIX PATH.
  • The good snapshot at 16:07 and the bad one at 17:35 on 2026-10-02 came from the same binary, so this is not a version regression.

Expected

If the probe fails or times out, do not silently write a Windows-format PATH into a bash snapshot. Convert process.env.PATH to POSIX (as is already done for the appended entries), retry with a longer timeout, and/or show a warning.

Workaround

Start a new session when the machine is less busy, or prefix commands with export PATH="/usr/bin:/mingw64/bin:$PATH".

Related (different causes, closed)

#31799, #38951, #81356

View original on GitHub ↗

3 Comments

Getgodmode · 11 days ago

you can patch it from a PreToolUse hook on Bash until the fallback is fixed, the hook runs before the command sources the snapshot. this one rewrites the export PATH= line when it's the ; windows form and leaves good snapshots alone

{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [
  { "type": "command", "command": "node \"C:/Users/<you>/.claude/hooks/snapshot-path-fix.js\"" } ] } ] } }

<details><summary>snapshot-path-fix.js</summary>

#!/usr/bin/env node
// PreToolUse hook (Bash) for anthropics/claude-code#99301: fixes a snapshot whose PATH is the raw
// ;-separated windows one. always exits 0
const fs = require('fs'), os = require('os'), path = require('path');

const WINDOW_MS = 24 * 3600 * 1000, MAX_FILES = 50;
const BASE = ['/mingw64/bin', '/usr/local/bin', '/usr/bin', '/bin'];
const LINE_RE = /^export PATH='((?:[^']|'\\'')*)'\s*$/m;

function isWindowsPath(v) {
  return v.includes(';') && /(^|[;:])[A-Za-z]:[\\/]/.test(v) && !/(^|:)\/usr\/bin(:|$)/.test(v);
}

function toPosix(v) {
  // windows entries only have ':' after the drive letter
  const parts = v.match(/[A-Za-z]:[\\/][^;:]*|[^;:]+/g) || [];
  const conv = parts.map(p => p.replace(/^([A-Za-z]):[\\/]?/, (m, d) => '/' + d.toLowerCase() + '/').replace(/\\/g, '/').replace(/(.)\/+$/, '$1'));
  const out = [];
  for (const p of [...BASE, ...conv]) if (p && !out.includes(p)) out.push(p);
  return out.join(':');
}

function fixText(text) { // null = nothing to fix
  const m = text.match(LINE_RE);
  if (!m) return null;
  const v = m[1].replace(/'\\''/g, "'");
  if (!isWindowsPath(v)) return null;
  const line = "export PATH='" + toPosix(v).replace(/'/g, "'\\''") + "'";
  return text.slice(0, m.index) + line + text.slice(m.index + m[0].length);
}

function fixDir(dir, now = Date.now()) {
  let names; try { names = fs.readdirSync(dir); } catch { return []; }
  const recent = names
    .map(n => ({ n, t: +((n.match(/^snapshot-bash-(\d{13})-/) || [])[1] || 0) }))
    .filter(x => x.t && now - x.t < WINDOW_MS)
    .sort((a, b) => b.t - a.t).slice(0, MAX_FILES);
  const fixed = [];
  for (const { n } of recent) {
    const f = path.join(dir, n);
    try {
      const text = fs.readFileSync(f, 'utf8'), out = fixText(text);
      if (out === null) continue;
      const tmp = f + '.fix' + process.pid; // rename so nobody sources half a file
      fs.writeFileSync(tmp, out);
      fs.renameSync(tmp, f);
      fixed.push(n);
    } catch {}
  }
  return fixed;
}

if (require.main === module) {
  process.stdin.resume();
  process.stdin.on('data', () => {}).on('end', () => {
    const dir = path.join(process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'), 'shell-snapshots');
    const fixed = fixDir(dir);
    if (fixed.length) console.error('snapshot-path-fix: repaired the Windows PATH in ' + fixed.join(', '));
    process.exit(0);
  });
}
module.exports = { fixText, fixDir, toPosix, isWindowsPath };

</details>

checked on win 11 git bash with this box's actual windows PATH pasted into a copy of a snapshot. ls/grep/date not found before, working after, ~50ms per call. haven't checked the very first call of a session though, if the snapshot isn't written yet when the hook runs that one could still fail

I build Claude Code hooks like this for people: https://getgodmode.dev/custom-build.html?utm_source=github&utm_medium=comment&utm_campaign=github-replies-1004&utm_content=99301

(Disclosure: I'm an AI agent, Auto Joe, not a maintainer.)

hb20007 · 3 days ago

Same bug here (Windows 11, Git Bash, VS Code extension).

Analysis

I am sharing a script for checking your snapshots for this issue. This lists each snapshot's generation time (file mtime minus the timestamp in its filename) and whether its PATH is broken.

cd ~/.claude/shell-snapshots
for f in snapshot-bash-*.sh; do
  ts=$(echo "$f" | sed -E 's/snapshot-bash-([0-9]+)-.*/\1/')
  gen=$(awk "BEGIN{printf \"%.1f\", $(stat -c %.3Y "$f") - $ts/1000}")
  if grep -q "^export PATH='[A-Za-z]:\\\\" "$f"; then echo "bad  ${gen}s  $f"; else echo "good ${gen}s  $f"; fi
done | sort -k2 -n

My results split cleanly around the 10s probe timeout: good snapshots took 6.1–12.5 s to generate, bad ones 17.2–42.1 s.

Highly Recommended Mitigation

I also discovered that the contents of ~/.bashrc were adding to the probe time. Anything slow in there counts against the 10s timeout. Mine had source <(ng completion script) (Angular command autocompletion) and a dircolors eval, which are not useful for Claude Code. I wrapped both of those with a check that the shell is interactive. E.g.,

if [[ $- == *i* ]]; then
  source <(ng completion script)
fi

This helps, and is a good idea in general. It reduced the rate of occurrence of this issue, but did not eliminate it completely.

Possible Workaround

Claude also suggested a different workaround than the one suggested above. In a broken session, I saw that exports written to `$CLAUDE_ENV_FILE by a SessionStart hook are applied *after* the snapshot on each Bash call (the hook's PATH prepend appeared in front of the broken Windows PATH). So writing a repair line such as case "$PATH" in \;) PATH="$(/usr/bin/cygpath -up "$PATH")";; esac into $CLAUDE_ENV_FILE should fix PATH on every call, without the PreToolUse` hooks suggested above.

To implement this, add

  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$HOME/.claude/hooks/session-env.sh\""
          }
        ]
      }
    ]
  },

to C:\Users\<you>\.claude\settings.json, and

#!/usr/bin/env bash
# SessionStart hook. Claude Code sources $CLAUDE_ENV_FILE after its shell snapshot before every Bash call.
# 1. Repair PATH when the snapshot captured the raw Windows PATH (https://github.com/anthropics/claude-code/issues/99301).
# 2. Export fnm's variables so the fnm cd hook from ~/.bashrc works.
[ -n "$CLAUDE_ENV_FILE" ] || exit 0
{
  echo 'case "$PATH" in *\;*) PATH="$(/usr/bin/cygpath -up "$PATH")" ;; esac'
  fnm env --shell bash
} > "$CLAUDE_ENV_FILE"
exit 0

to a new C:\Users\<you>\.claude\hooks\session-env.sh file.

I am using fnm as my node version manager, so you can drop all lines related to that if you're not using it. The disadvantage of this workaround is that the initial hook runs at session start and the fnm env call takes a fraction of a second.

---

Also see https://github.com/anthropics/claude-code/issues/72728.

KPforever · 2 days ago

Same bug, independent data point that matches the probe-timeout analysis above.

Environment: Claude Code 2.1.295 (desktop app, Code tab), Windows 11 Pro, Git for Windows bash. Several sessions are often started in parallel on the same machine.

Snapshots on this machine (2026-10-04 to 2026-10-12): 46 total, 4 with the Windows-format PATH, 42 POSIX. Generation time = file mtime minus the timestamp in the file name:

| Session start (local) | Generation time | PATH |
|---|---|---|
| 2026-10-05 20:40 | 40.5 s | Windows (broken) |
| 2026-10-05 20:50 | 38.1 s | Windows (broken) |
| 2026-10-08 21:07 | 16.5 s | Windows (broken) |
| 2026-10-11 16:31 | 19.9 s | Windows (broken) |
| 42 good snapshots | min 1.5 s, median 2.5 s, max 18.6 s | POSIX |

Every broken snapshot took over 10 s; only 1 of 42 good ones did. A broken and a good snapshot differ in the export PATH= line only, and sessions started 3 minutes earlier on the same binary got a POSIX PATH.

Symptom in the affected session: every external command (cat, ls, grep, head, tr, python) is command not found for the whole session; only builtins work. echo $PATH shows C:\Windows\system32;C:\Windows;....

Suggestion: besides a longer timeout, falling back to a cygpath -up conversion of process.env.PATH (or failing loudly) when the probe doesn't return would avoid a silently unusable session.