Windows/Git Bash: 85 per-function base64 evals make every Bash call pay ~5-9s; snapshot-creation 10s timeout silently forks sessions into fast/slow regimes

Status Open
Reported on v2.1.237
Maintainer reply None cached
Activity 0 comments · opened Aug 25, 2026

Summary

On Windows + Git for Windows (MSYS bash), the Bash tool's shell-snapshot mechanism adds ~5–9 seconds to every Bash tool call, and the snapshot-creation path has a timeout asymmetry that silently splits sessions into two performance regimes. Two coupled problems, both measured:

  1. Per-call cost: the snapshot stores every captured shell function as its own eval "$(echo '<base64>' | base64 -d)" > /dev/null 2>&1 line. On a machine whose login shell defines 145 functions (stock Git-for-Windows git-completion.bash — 144 of the 145), that is 85 base64-eval lines, each paying an MSYS subshell fork (~16 ms) plus an external base64 exec (~65 ms with real-time AV on). Re-sourced on every Bash tool call.
  2. Creation-timeout roulette: snapshot creation runs the login shell under a hard 10,000 ms timeout, while creation itself takes ~6 s idle on this class of machine. Under load it times out → the session permanently falls back to per-call bash -c -l (the -l is added only when no snapshot was captured) — which is faster (~1.3 s/call) than snapshot mode (~6.5 s/call). Sessions randomly land in one regime at their first Bash call and stay there. A timed-out creation can still leave the snapshot file on disk, which makes forensics confusing (file present, never sourced).

Environment

  • Windows 11 Pro, Git for Windows (stock /etc/profile.d/git-prompt.sh → git-completion), Windows Defender real-time on
  • Claude Code 2.1.237 (desktop app and CLI; behavior identical), snapshot files ~/.claude/shell-snapshots/snapshot-bash-*.sh, 88,199 bytes / 85 evals each

Measurements

Sourcing cost isolated (same snapshot, same machine, idle):

source full snapshot (85 evals) : 5,703 – 6,510 ms
same file, eval lines stripped  :        88 ms      <- 98.5% of cost is the eval lines
85 × $(:) fork-only control     :     1,393 ms      <- pure MSYS fork floor, AV-independent
85 × $(echo|base64 -d)          :     6,853 ms      (~81 ms/eval)

Corpus evidence (stream-parsed 9,473 clean serial Bash tool_use→tool_result deltas across 110 session transcripts / 35 days, one machine):

  • Sessions in snapshot mode: floors 6.2–8.4 s, medians 8–15 s under load
  • Sessions in -l fallback mode: floors 972–1,300 ms
  • 39 of 83 sessions (n≥5) sat under a 2 s floor — i.e. the busier the machine, the more sessions accidentally escape the tax, which makes the cost look nondeterministic and very hard for users to attribute
  • First call of a session ≈ 2× median (creation) — consistent with the 10 s deadline being marginal, not comfortable

Suggested fixes (either helps; both are small)

  1. Batch the decode: store all function bodies in one base64 blob (or plain declare -f text — which #50733 already requests for auditability) and decode once. Turns 85 forks+execs into ~1. Expected: ~6 s → well under 200 ms per call on this machine class.
  2. Rationalize the creation timeout: either raise/scale it, or make the outcome consistent — right now timing out yields a faster session than succeeding, which is exactly backwards; and the partially-written snapshot file should be removed on timeout so file-presence matches behavior.

Workaround for affected users (validated)

Git for Windows' /etc/profile.d/git-prompt.sh sources ~/.config/git/git-prompt.sh instead of its completion-loading block when that file exists. A user-level override that replicates the stock prompt but skips git-completion.bash when CLAUDECODE=1 is set (Claude Code sets it on the snapshot-creation shell) drops the snapshot to ~1 eval / 4.9 KB with the interactive prompt byte-identical. After deploying it: per-call floors converge to ~1.0–1.3 s in every new session. Happy to share the override file in a comment if useful.

View original on GitHub ↗