zsh shell snapshot drops single-underscore helper functions and causes gvm startup noise

Resolved 💬 2 comments Opened Mar 29, 2026 by Flynnon Closed Mar 29, 2026

Summary

Claude Code on zsh can emit startup noise when using shell snapshots with environments that source gvm.

Impact

When Claude restores a zsh shell from a generated snapshot, the snapshot can retain gvm functions like setValueForKeyFakeAssocArray, valueForKeyFakeAssocArray, __gvm_read_environment_file, and cd, but omit their required single-underscore helpers _encode and _decode. This leaves the restored shell in a partially-defined state and can produce command-not-found noise during shell initialization.

Environment

  • Claude Code session shell: /bin/zsh
  • Repo: Flynnon/remote_claude
  • Observed snapshot file: ~/.claude/shell-snapshots/snapshot-zsh-1774763716489-ft5eg9.sh
  • User shell init sources gvm from ~/.zshrc

Symptoms

The noisy startup was traced to gvm's fake associative-array helpers. The shell reports missing helper functions during startup / directory-change hooks, e.g.:

setValueForKeyFakeAssocArray:27: command not found: _encode
valueForKeyFakeAssocArray:28: command not found: _decode

Root Cause Analysis

This is not caused by .zshrc itself, and it is not caused by gvm failing under a normal interactive zsh startup.

The failure only appears in Claude's restored shell snapshot state:

  1. ~/.zshrc sources gvm.
  2. gvm overrides cd() and uses __gvm_read_environment_file().
  3. __gvm_read_environment_file() calls setValueForKeyFakeAssocArray() / valueForKeyFakeAssocArray().
  4. Those functions depend on _encode() / _decode() from ~/.gvm/scripts/function/_bash_pseudo_hash.
  5. The generated Claude snapshot contains the higher-level functions but omits _encode() / _decode().
  6. After restore, the shell is inconsistent: callers exist, helper functions do not.

Evidence from the snapshot:

  • Present in snapshot:
  • setValueForKeyFakeAssocArray ()
  • valueForKeyFakeAssocArray ()
  • __gvm_read_environment_file ()
  • cd ()
  • Missing from snapshot:
  • _encode ()
  • _decode ()

Additional signal: the snapshot preserved many __double_underscore functions, but effectively filtered out single_underscore helpers. In the affected snapshot there were 17 __... functions but only 1 _... function, while gvm's source helper file defines both _encode and _decode alongside the exported wrappers.

Reproduction

A minimal repro shape is:

  1. Use zsh as Claude Code's shell.
  2. Source gvm from ~/.zshrc.
  3. Let Claude create and reuse a shell snapshot.
  4. Restore from that snapshot.
  5. Observe missing _encode / _decode while gvm wrapper functions are still present.

You can inspect the affected snapshot directly and compare it with gvm source:

python3 - <<'PY2'
from pathlib import Path
snap = Path.home()/'.claude/shell-snapshots/snapshot-zsh-1774763716489-ft5eg9.sh'
text = snap.read_text(encoding='utf-8', errors='ignore')
for needle in [
    'setValueForKeyFakeAssocArray ()',
    'valueForKeyFakeAssocArray ()',
    '__gvm_read_environment_file ()',
    'cd ()',
    '_encode ()',
    '_decode ()',
]:
    print(needle, needle in text)
PY2

Expected:

  • If a function's callers are snapshotted, its helper functions should also be present.

Actual:

  • Wrapper/caller functions are present, but _encode / _decode are missing.

Why this matters

The snapshot mechanism appears to be serializing only part of the shell function graph. Filtering out single-underscore helpers breaks valid shell setups that depend on private helper names.

Suggested fix direction

Please review the shell snapshot generation / restore logic for function-name filtering. In particular, verify whether functions beginning with a single underscore are being dropped intentionally. If so, that filter is too aggressive: private helper functions can still be required by restored public or semi-public functions.

A safe invariant would be:

  • if a function is persisted into the snapshot, any functions it references that are defined in the same captured shell state should not be silently dropped based on naming convention alone.

View original on GitHub ↗

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