BASH_FUNC_grep / BASH_FUNC_find wrappers break under `set -u` (unguarded $ZSH_VERSION)
Summary
The grep and find bash function wrappers Claude Code injects via the shell-snapshot mechanism reference $ZSH_VERSION without the ${ZSH_VERSION:-} default-value guard. Any subprocess that runs set -u (e.g. a strict git hook) and pipes through grep/find triggers an unbound variable error, which pipefail propagates as pipe failure. Real-world impact: strict commit-msg hooks reject every commit with a confusing stderr trail, even when the message is well-formed.
Reproduction
- In a Claude Code session, confirm wrapper presence:
declare -f grep - Create a
commit-msghook with the standard strict prelude:
``bash``
#!/usr/bin/env bash
set -euo pipefail
COMMIT_MSG=$(cat "$1")
echo "$COMMIT_MSG" | grep -qE '[A-Z]{2,}-[0-9]+' || { echo "missing ticket ref"; exit 1; }
- `git commit -m "fix: foo
ABC-123"`
Expected: hook passes (message contains ABC-123).
Actual: hook fails with environment: line 6: ZSH_VERSION: unbound variable on every grep invocation. set -u + pipefail cause the pipe to return non-zero before grep ever sees stdin, so the -qE check is never actually evaluated against the message.
Root cause
In the wrapper template (bundled JS function, minified name uJ8, in the claude Mach-O binary). The emitted bash function looks like this — line 5 is the bug:
function grep {
local _cc_bin="${CLAUDE_CODE_EXECPATH:-}" # correctly guarded
[[ -x $_cc_bin ]] || _cc_bin=$(command -v claude 2>/dev/null)
if [[ ! -x $_cc_bin ]]; then command grep "$@"; return; fi
if [[ -n $ZSH_VERSION ]]; then # ← unguarded — errors under set -u
ARGV0=ugrep "$_cc_bin" -G --ignore-files --hidden -I --exclude-dir=.git ... "$@"
elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then
ARGV0=ugrep "$_cc_bin" -G ... "$@"
elif [[ $BASHPID != $$ ]]; then
exec -a ugrep "$_cc_bin" -G ... "$@"
else
(exec -a ugrep "$_cc_bin" -G ... "$@")
fi
}
The very next line above uses ${VAR:-} correctly, so the idiom is already known to the author of this template — this looks like an oversight on a single line. The same template generates the find wrapper (BASH_FUNC_find%%) and has the same bug.
Fix
In the JS template, change:
if [[ -n $ZSH_VERSION ]]; then
to:
if [[ -n ${ZSH_VERSION:-} ]]; then
Workarounds (not a real fix)
- Per-invocation:
ZSH_VERSION='' git commit ... - Shell-rc
export ZSH_VERSION=''— rejected, masks the real bug and pollutes the env namespace.
Environment
- Claude Code:
2.1.121(also reproduces on2.1.118–2.1.120— same template bytes). - macOS / arm64 / bash 5 (
/usr/local/bin/bash). - Wrapper bytes verified via
declare -f grepandenv | grep BASH_FUNC_. - Buggy literal
if [[ -n $ZSH_VERSION ]]; thenappears 3× in the binary (deduplicated string constants + main/worker copies); one logical bug, one logical fix.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗