Bash tool: zsh wrappers persist after pipe-truncated commands → zombie processes accumulate and block subsequent git operations
Summary
The Bash tool wraps every command in a /bin/zsh -c ... eval '<cmd>' ... wrapper. When the wrapped command exits via SIGPIPE (e.g. … | head -N) or is a polling loop, the outer zsh wrapper does not terminate and persists indefinitely. These wrappers accumulate across sessions and have been observed to interfere with subsequent git operations from the same repository (commands hang or report locks even when the user issued no concurrent git).
Environment
- App: Claude Code 1.8089.1 (macOS desktop)
- OS: macOS (Apple Silicon)
- Shell wrapper used by Bash tool:
/bin/zsh -c source <snapshot>.sh ... && eval '<command>' < /dev/null && pwd -P >| /tmp/claude-XXXX-cwd
Live evidence (captured today on user machine)
PID ETIME COMMAND
85079 02:20 /bin/zsh -c ... eval 'go build ./... 2>&1 | head -20; echo "EXIT=$?"' < /dev/null && pwd -P ...
This wrapper belongs to a previous Claude Code session (CODEX_COMPANION_SESSION_ID=057356c9-...). The go build finished long ago but the surrounding zsh wrapper is still running 2+ minutes later.
In addition, nine separate sleep 30 processes from various parent PIDs are alive concurrently — leftover polling loops from past Bash invocations:
88065 (ppid 24921) sleep 30 00:25
88100 (ppid 11410) sleep 30 00:23
88136 (ppid 36036) sleep 30 00:23
88146 (ppid 52300) sleep 30 00:22
88288 (ppid 11469) sleep 30 00:19
88481 (ppid 17910) sleep 30 00:11
88631 (ppid 39532) sleep 30 00:05
88698 (ppid 18883) sleep 30 00:01
88741 (ppid 34695) sleep 30 00:01
.git/index.lock files are not present — i.e. the lock is not stale on disk; the contention comes from living wrappers still holding repo state or from concurrent invocations colliding because previous ones never finished.
Symptoms reported by user
gitoperations (status / fetch / push) intermittently rack/hang, then a subsequentgitreports another git process running or returns errors.- Need to manually
killorphan wrappers to unblock workflow. - The number of orphans grows over the day, requiring repeated cleanup.
- The user has had to add an explicit
🔴 CRITICALrule to their globalCLAUDE.mdforbiddingrun_in_background:true,&, anduntil-sleeppolling — purely to mitigate this.
Suggested fixes
- Propagate signals properly: Bash tool wrapper should forward SIGPIPE/SIGTERM/SIGHUP to the eval'd command, and exit when the inner command exits (regardless of stdout-pipe state).
- Orphan cleanup: when a Claude Code session exits, reap all wrappers tagged with that
CODEX_COMPANION_SESSION_ID. - Grace period termination: if a wrapper has no active foreground child for N seconds, self-terminate.
claude doctorsubcommand: list and optionally kill orphan wrappers belonging to dead sessions.
Reproduction (intermittent but repeatable)
- In Claude Code, invoke a Bash tool call whose command pipes into a truncating consumer, e.g.
go build ./... 2>&1 | head -20. - After the Bash call returns, in a separate terminal run
ps -eo pid,etime,command | grep "zsh -c"— observe the wrapper process still alive with growing etime. - Open another Claude Code session in the same repo and run
git statusshortly after — sometimes hangs.
Workaround in use
User has had to add explicit project- and user-level rules in CLAUDE.md and project memory:
- ❌
run_in_background: true - ❌
&(background) - ❌
until [ -e <file> ]; do sleep N; done - ❌
tail -f &,watch & - ✅ foreground only, with explicit
timeout(≤ 10 minutes)
This works around the symptom but is a heavy guardrail that should not be necessary.
---
Reported via Claude Code session at the user's explicit request.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗