Session hangs when bash subprocess becomes zombie (defunct)
Description
Claude Code sessions can become permanently stuck when a bash subprocess terminates abnormally and becomes a zombie process. The parent Claude process fails to reap the zombie child, causing the entire session to hang indefinitely.
Steps to Reproduce
- Start a Claude Code session with
--allow-dangerously-skip-permissions - Run long-running bash commands (e.g., filesystem operations, build scripts, flashing operations)
- If a subprocess terminates abnormally (e.g., due to signals, I/O errors), it may become a zombie
- The Claude session becomes unresponsive
Observed Behavior
$ ps aux | grep claude
patane 1357509 78.5 6.6 ... Rl+ ... claude --allow-dangerously-skip-permissions
$ ps aux | grep defunct
patane 1371523 0.0 0.0 0 0 ? Zs 07:02 0:00 [bash] <defunct>
$ ps -eo pid,ppid,stat | grep 1371523
1371523 1357509 Z
The zombie bash process (PID 1371523) is a child of the Claude process (PID 1357509). The Claude session is stuck and unresponsive, requiring kill -9 to terminate.
Expected Behavior
Claude should properly handle SIGCHLD signals and reap zombie children, or implement a timeout mechanism to detect and recover from stuck states.
Environment
- OS: Ubuntu 24.04 (Linux 6.17.0-8-generic)
- Claude Code version: Latest (via npm)
- Node.js: v20+
- Shell: bash
Workaround
We've implemented a watchdog service that monitors for stuck Claude sessions and automatically kills them:
1. Watchdog Script (~/.local/bin/claude-watchdog)
#!/bin/bash
# Claude Watchdog - Monitors and cleans up stuck Claude sessions
LOG_FILE="${HOME}/.claude/watchdog.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
check_zombies() {
for claude_pid in $(pgrep -f "claude.*--allow"); do
zombie_count=$(ps --ppid "$claude_pid" -o stat= 2>/dev/null | grep -c "Z")
if [ "$zombie_count" -gt 0 ]; then
log "WARNING: Claude PID $claude_pid has $zombie_count zombie children"
cpu_usage=$(ps -p "$claude_pid" -o %cpu= 2>/dev/null | tr -d ' ')
if [ -n "$cpu_usage" ] && (( $(echo "$cpu_usage < 1" | bc -l) )); then
log "Claude PID $claude_pid appears stuck (CPU: ${cpu_usage}%)"
return 1
fi
fi
done
return 0
}
kill_stuck_sessions() {
for claude_pid in $(pgrep -f "claude.*--allow"); do
zombie_count=$(ps --ppid "$claude_pid" -o stat= 2>/dev/null | grep -c "Z")
cpu_usage=$(ps -p "$claude_pid" -o %cpu= 2>/dev/null | tr -d ' ')
if [ "$zombie_count" -gt 0 ] && [ -n "$cpu_usage" ] && (( $(echo "$cpu_usage < 1" | bc -l) )); then
log "KILLING stuck Claude session PID $claude_pid"
kill -9 "$claude_pid" 2>/dev/null
fi
done
}
# Daemon mode
log "Starting Claude watchdog daemon"
while true; do
if ! check_zombies; then
kill_stuck_sessions
fi
sleep 60
done
2. Systemd User Service (~/.config/systemd/user/claude-watchdog.service)
[Unit]
Description=Claude Code Watchdog - Monitor and clean stuck sessions
After=default.target
[Service]
Type=simple
ExecStart=%h/.local/bin/claude-watchdog --daemon
Restart=on-failure
RestartSec=10
[Install]
WantedBy=default.target
Enable with:
systemctl --user daemon-reload
systemctl --user enable --now claude-watchdog.service
Suggested Fix
- Implement proper SIGCHLD signal handling in the Node.js process to reap zombie children
- Add a subprocess timeout mechanism for bash commands
- Consider using
child_processoptions likedetached: falseand proper cleanup on exit - Implement a health check that detects unresponsive states and auto-recovers
Additional Context
This issue occurs more frequently during:
- Long-running build/compile operations
- Filesystem operations on mounted drives
- Operations that spawn multiple subprocesses
- When using
--allow-dangerously-skip-permissionsmode
This issue has 6 comments on GitHub. Read the full discussion on GitHub ↗