Windows/Git Bash: shell snapshot replays 88 base64 evals per Bash tool call, ~2.3s fixed overhead on every command
What happened?
On Windows with Git Bash, every Bash tool call pays a fixed ~2.3 s of pure fork overhead before the command runs. The cost is sourcing the shell snapshot, not spawning the shell and not the command itself.
This is the same mechanism as #31437 (macOS, closed as stale) and #19585 (Windows, closed as stale), but the Windows arithmetic is worse and the trigger needs no unusual user config — a stock Git for Windows install is enough.
Measurements (Git for Windows bash.exe, no EDR exclusions changed, machine freshly booted):
| step | time |
|---|---|
| bare bash -c exit | 53 ms |
| bash -c "source <snapshot>" (91 KB, 88 evals) | 2343 ms |
| same after reducing captured functions to 6 | 175 ms |
So spawn is healthy (53 ms) — this is not the MSYS2 spawn degradation in #80670. The entire delay is snapshot replay.
Why 88 functions on a stock install: Git for Windows ships /etc/profile.d/git-prompt.sh, which sources git-completion.bash. A login shell therefore defines 148 functions before ~/.bashrc is ever read:
$ bash -lc 'declare -F | wc -l'
148
$ bash -c 'declare -F | wc -l'
0
The snapshot's capture filter (grep -vE '^_[^_]') drops the _git_add-style names but keeps all 88 __git_* / __gitcomp_* ones. Each is replayed as:
eval "$(echo '<base64>' | base64 -d)" > /dev/null 2>&1
That is two forks per function. MSYS2 fork emulation costs ~27 ms/fork here, so 88 functions ≈ 2.3 s, paid on every Bash tool call for the entire session.
Confirmed the cost is fork count, not function content — 88 evals of a trivial f1 () { :; } body reproduce it exactly:
8 evals: 270 ms
88 evals: 2343 ms
bare spawn: 53 ms
These are all shell completion functions. They are dead weight in a non-interactive bash -c "<command>" context — no readline, no TAB, never invoked.
What did you expect to happen?
Bash tool calls should not carry a multi-second constant. Snapshot replay should not be O(forks) in the number of captured functions, and interactive-only completion functions pulled in by /etc/profile.d should not be captured at all.
Steps to reproduce
- Windows 11, stock Git for Windows (nothing added to
~/.bashrc). - Start Claude Code, run any Bash tool command (
echo hi). - Observe ~2 s between the command appearing and it executing. Permission mode is irrelevant — reproduces under bypass, where no permission check runs.
- Confirm directly:
snap=$(ls -t ~/.claude/shell-snapshots/*.sh | head -1)
grep -c 'base64 -d' "$snap" # 88
time bash -c "source $snap" # ~2.3 s
time bash -c exit # ~0.05 s
Suggested fixes
- Don't fork per function.
declare -f "$f" >> snapshotwrites the definition directly; sourcing it costs zero forks. The base64+eval wrapper exists for fault isolation on bash 3.2 (macOS/bin/sh), which does not apply to Git Bash — bash 4+ can take the direct path. This alone turns 2.3 s into single-digit ms. (Same suggestion as #31437, which was closed without a fix.) - Don't capture completion functions. They cannot be used by a non-interactive tool call. Filtering
^_*git-style completion names, or anything registered viacomplete -F, would cut the capture set to the handful of functions users actually define.
Workaround for others hitting this
Appending a non-interactive guard to ~/.bashrc (sourced after /etc/profile.d/*, so it can undo them) drops capture from 88 functions to 6 and snapshot cost from 2343 ms to 175 ms, while interactive Git Bash keeps its completions and __git_ps1 prompt:
case $- in
*i*) ;;
*) unset -f $(declare -F | awk '{print $3}' | grep '^_\+git') 2>/dev/null || true ;;
esac
Claude Code version
2.1.220
Operating system
Windows 11 (26200), Git for Windows bash, native claude.exe install
Related
- #31437 — same mechanism, macOS, ~8 s/call, closed as stale
- #19585 — Windows, 30-90 s/call, closed as stale
- #80670 — Windows Bash slowness from MSYS2 spawn degradation; different cause (spawn was 53 ms here)
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗