claude-code - 💡(How to fix) Fix Bash tool: zsh wrappers persist after pipe-truncated commands → zombie processes accumulate and block subsequent git operations

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

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).

Root Cause

.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.

Fix Action

Fix / Workaround

Workaround in use

Code Example

PID    ETIME    COMMAND
85079  02:20    /bin/zsh -c ... eval 'go build ./... 2>&1 | head -20; echo "EXIT=$?"' < /dev/null && pwd -P ...

---

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
RAW_BUFFERClick to expand / collapse

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

  • git operations (status / fetch / push) intermittently rack/hang, then a subsequent git reports another git process running or returns errors.
  • Need to manually kill orphan wrappers to unblock workflow.
  • The number of orphans grows over the day, requiring repeated cleanup.
  • The user has had to add an explicit 🔴 CRITICAL rule to their global CLAUDE.md forbidding run_in_background:true, &, and until-sleep polling — purely to mitigate this.

Suggested fixes

  1. 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).
  2. Orphan cleanup: when a Claude Code session exits, reap all wrappers tagged with that CODEX_COMPANION_SESSION_ID.
  3. Grace period termination: if a wrapper has no active foreground child for N seconds, self-terminate.
  4. claude doctor subcommand: list and optionally kill orphan wrappers belonging to dead sessions.

Reproduction (intermittent but repeatable)

  1. In Claude Code, invoke a Bash tool call whose command pipes into a truncating consumer, e.g. go build ./... 2>&1 | head -20.
  2. 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.
  3. Open another Claude Code session in the same repo and run git status shortly 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.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix Bash tool: zsh wrappers persist after pipe-truncated commands → zombie processes accumulate and block subsequent git operations