Shell snapshot still drops underscore-prefixed zsh helper functions in v2.1.112 (recurrence of #40602)

Resolved 💬 4 comments Opened May 3, 2026 by madmansn0w Closed May 3, 2026

Summary

Recurrence of #40602 ("zsh shell snapshot drops single-underscore helper functions"), which was closed on 2026-03-29 but is still reproducible in Claude Code v2.1.112 (current release as of 2026-05-03). The shell snapshotter at ~/.claude/shell-snapshots/snapshot-zsh-*.sh continues to omit zsh functions whose names start with _, even when they are dispatched to from a public wrapper that is captured. This silently breaks every shell command whose wrapper depends on a private helper.

Reproduction

Minimal ~/.zshrc:

_my_helper() { echo "hi from helper"; }
my_cmd() { _my_helper "$@"; }

Restart Claude Code so the snapshot regenerates. Then from any Bash tool call:

$ my_cmd
my_cmd:1: command not found: _my_helper

Verify the snapshot:

$ grep -c '^my_cmd ()' ~/.claude/shell-snapshots/snapshot-zsh-*.sh
1
$ grep -c '^_my_helper ()' ~/.claude/shell-snapshots/snapshot-zsh-*.sh
0

The wrapper is captured; the underscore-prefixed helper is not.

Empirical evidence from a real user setup

~/.zshrc defines five public wrappers that dispatch to underscore-prefixed helpers:

| Wrapper (line) | Helper(s) called | Captured in snapshot? |
|---|---|---|
| wt() (747) | _wt_new, _wt_clean, _wt_list, _wt_next_num | wrapper yes, helpers no |
| chruby() (178) | _lazy_load_chruby | wrapper yes, helper no |
| ruby() (179) | _lazy_load_chruby | wrapper yes, helper no |
| gem() (180) | _lazy_load_chruby | wrapper yes, helper no |
| bundle() (181) | _lazy_load_chruby | wrapper yes, helper no |

Verification commands run against the live snapshot (snapshot-zsh-1777127572216-57d75s.sh):

$ for fn in _lazy_load_chruby _wt_list _wt_next_num _wt_new _wt_clean; do
    echo "$fn: $(grep -cE "^${fn}\s*\(\)" ~/.claude/shell-snapshots/snapshot-zsh-*.sh)"
  done
_lazy_load_chruby: 0
_wt_list: 0
_wt_next_num: 0
_wt_new: 0
_wt_clean: 0

$ for fn in chruby ruby gem bundle wt; do
    echo "$fn: $(grep -cE "^${fn}\s*\(\)" ~/.claude/shell-snapshots/snapshot-zsh-*.sh)"
  done
chruby: 1
ruby: 1
gem: 1
bundle: 1
wt: 1

All five wrappers fail at runtime when invoked from Claude Code's Bash tool. Same observed pattern as #40602 and #47792 (both closed-as-duplicate); the previously-reported _encode/_decode (GVM) and _bash_pseudo_hash cases are instances of this same drop.

Expected behavior

Per the recommendation in #47792: the snapshotter should either (a) capture all defined functions including underscore-prefixed helpers, or (b) re-source the user's shell init files so wrappers can resolve their dependencies at runtime.

The current behavior is silent_drop: the wrapper is present, looks functional via type wt, but fails immediately on invocation because the dispatcher target is missing. There is no warning at snapshot time, no warning at invocation time beyond zsh's generic "command not found", and no way for a user to know which of their commands are silently broken until they try one.

Impact

Any user-defined zsh function following the public-wrapper / private-helper convention (a very common pattern for lazy-loading version managers like chruby, nvm, pyenv; for command dispatchers; for project-local CLIs) will silently break inside Claude Code while continuing to work in a normal terminal. Users who don't realize the snapshot diverges from their interactive shell get hard-to-diagnose command not found: _foo errors with no obvious root cause.

Environment

  • Claude Code version: 2.1.112 (binary at ~/.local/share/claude/versions/2.1.112, Mach-O arm64)
  • Shell: zsh (default macOS)
  • OS: macOS Darwin 24.6.0
  • Snapshot: ~/.claude/shell-snapshots/snapshot-zsh-1777127572216-57d75s.sh

References

  • #40602 — original report, closed 2026-03-29 (this is a recurrence)
  • #47792 — duplicate of #40602, GVM-specific case
  • #46856 — adjacent: snapshot includes unresolvable autoload stubs
  • #47978 — adjacent: snapshot captures mise functions without dependencies

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗