[BUG] Shell snapshot captures interactive-only functions, eval-per-function pattern causes O(n) fork overhead (~8s per Bash tool call)
What happened?
Every Bash tool invocation takes ~8 seconds due to two compounding issues in the shell snapshot mechanism:
- Snapshot creation captures shell functions that a standard non-interactive shell would not have.
bash -l -c 'declare -F | wc -l'returns 5 functions, but the snapshot contains 199. This suggests the snapshot process sources.bashrcin a way that bypasses the standard non-interactive guard (case $- in *i*) ...), picking up functions (mostly completions) that only belong in interactive shells.
- Each captured function is replayed via
eval "$(echo '<base64>' | base64 -d)", which forks a process per function (~40ms each). With 199 functions this is 199 × 40ms ≈ 8s of pure fork overhead on every Bash tool call.
Shell snapshots (~/.claude/shell-snapshots/) grew from ~4KB / 4 eval statements (on 2.1.34) to 195KB / 199 eval statements (on 2.1.69). 194 of the 199 captured functions are shell completion functions (__git_*, __brew_*, __gh_*, __buf_*, __jira_*, __colima_*, __podman_*, etc.) that serve no purpose in the non-interactive Bash tool context.
What did you expect to happen?
Bash tool calls should execute near-instantly (as they did on 2.1.34 and earlier). The snapshot should not capture functions that aren't present in a standard non-interactive shell, and the replay mechanism should not have O(n) fork overhead per function.
Error messages / logs
Debug log (claude --debug) shows the timeline:
09:38:56.948 Creating shell snapshot for bash (/opt/homebrew/bin/bash)
09:38:58.534 Shell snapshot created successfully (194955 bytes)
09:38:58.535 Spawning shell without login (-l flag skipped)
09:39:06.537 FileHistory: Added snapshot ← 8 seconds later
Timing confirmation:
$ time bash -c 'source ~/.claude/shell-snapshots/<new_snapshot>.sh'
real 7.8s
$ time bash -c 'source ~/.claude/shell-snapshots/<old_snapshot>.sh'
real 0.26s
199 trivial base64 evals (empty function bodies) also take ~8s, confirming the cost is fork overhead, not function content.
Steps to reproduce
- Have a
.bashrcthat sources completions (e.g., gcloud, brew bash_completion) outside of a non-interactive guard, or without one entirely — this is common in default/generated bashrc files - Upgrade to Claude Code 2.1.69+
- Run any Bash tool command (e.g.,
!echo hi) - Observe ~8 second delay
The key condition is having functions defined in .bashrc outside of (or before) a non-interactive guard like case $- in *i*) ;; *) return;; esac — or having no such guard at all, which is common in default/generated bashrc files. Completion functions are the most common case since there are many of them, but any functions sourced there would be affected.
Is this a regression?
Yes
Last working version
2.1.34
Claude Code version
2.1.69 (also reproduces on 2.1.70)
Platform
Anthropic (Claude Max)
Operating system
macOS Darwin 25.3.0 (Apple Silicon)
Terminal / shell
bash 5.x (/opt/homebrew/bin/bash)
Additional information
Workaround: Ensure .bashrc has a non-interactive guard, and move any heavy function/completion sourcing below it:
# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return;;
esac
# Source completions and other interactive-only functions AFTER the guard
source /path/to/completions.sh
source /path/to/bash_completion.sh
Suggested fixes:
- Investigate why snapshot creation captures functions not present in
bash -l -c(non-interactive login shell) - Consider inlining decoded function bodies directly in the snapshot instead of the
eval "$(echo '...' | base64 -d)"pipe pattern, eliminating the per-function fork overhead entirely. Or just do one eval.
Related issues: #25016, #19585, #10181 — similar symptoms, no root cause identified
This issue has 6 comments on GitHub. Read the full discussion on GitHub ↗