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

Status Fixed / completed
Reported on v2.1.112
Maintainer reply None cached
Activity 4 comments · opened May 3, 2026 · 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 ↗

4 Comments

madmansn0w · 5 months ago

Clarification on the closure status of #40602: I phrased the original report as if #40602 had been fixed and regressed. On re-reading the issue, that's not what happened — the github-actions bot auto-classified #40602 as a duplicate of #35151 and the 3-day notice elapsed without a counter-comment, so the bot auto-closed it with stateReason=COMPLETED. There was no fix; the duplicate-routing artifact just produces a COMPLETED label.

The canonical bug (#35151 — "setValueForKeyFakeAssocArray showing up in almost all Bash calls in Claude Code") is still OPEN and unresolved. Treat this issue as fresh empirical evidence for #35151 with a more general scope (every public wrapper that dispatches to a single-underscore helper, not just GVM's setValueForKeyFakeAssocArray).

github-actions[bot] · 5 months ago

Found 1 possible duplicate issue:

  1. https://github.com/anthropics/claude-code/issues/35151

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

madmansn0w · 5 months ago

Closing as duplicate of #35151 per github-actions bot routing. Full evidence migrated to https://github.com/anthropics/claude-code/issues/35151#issuecomment-... (just posted). Tracking the canonical bug there.

github-actions[bot] · 3 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.