fix(cli): kill spawned claude binary when launcher exits (orphan / 'two cursors' bug)#1190
Open
johnmarktaylor91 wants to merge 1 commit intoslopus:mainfrom
Open
Conversation
The binary-file branch of runClaudeCli spawns claude with stdio:'inherit' and propagates child->parent exit, but installs no parent->child signal handlers and no parent-death detection. When the launcher process dies (terminal close, happy crash, kill -TERM, OOM, etc.) the spawned claude process keeps running, gets reparented to PID 1, and stays bound to the TTY's foreground process group. The next time happy starts a session in the same pane, a second claude joins the same /dev/pts/N. Both have '+' in STAT (Sl+) meaning both are in the foreground process group; the kernel race-distributes each keystroke between them, producing the 'two cursors' / 'characters alternating' symptom users have reported. Typing becomes impossible until the orphan is killed manually. Fix: in runClaudeCli's binary branch, install signal forwarders for SIGINT/SIGTERM/SIGHUP/SIGQUIT, an exit handler that sends SIGTERM to the child, and a 1Hz ppid watcher that fires when the launcher itself gets reparented to PID 1 (catches the case where the launcher's parent dies ungracefully without sending a signal down the chain). The ppid-watch interval is unref()'d so it doesn't keep the event loop alive past normal child exit. Verified locally with a synthetic launcher that mirrors runClaudeCli's spawn pattern. Without the patch: SIGTERM to launcher leaves child alive with PPID=1. With the patch: child dies on SIGTERM, SIGHUP, or parent SIGKILL within ~1s.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
When happy is installed against a binary
claude(Homebrew or the native installer at~/.local/share/claude/versions/<v>/), users intermittently report that typing produces characters at two alternating cursor positions in the same pane, making input impossible. Often described as "two cursors" or "tmux is broken." It isn't tmux.Root cause
runClaudeCliinpackages/happy-cli/scripts/claude_version_utils.cjshas two branches:.js/.cjsclaude:import(importUrl)-- runs in-process, no orphan possible.spawn(cliPath, args, { stdio: 'inherit' })with only achild.on('exit', ...)handler. No signal forwarders, no parent-death detection.When the launcher process dies for any reason -- terminal SIGHUP on pane close,
kill -TERM, an OOM, an uncaught exception in the launcher's fetch interceptor, or the parent (happy) crashing without sending signals down the chain -- the spawned claude binary keeps running. Linux reparents it to PID 1, but it stays in the TTY's foreground process group (STAT=Sl+).Next time happy launches a session in the same pane, a second claude binds to the same
/dev/pts/N. Both are in the foreground PG, so the kernel race-distributes each keystroke between them. That's the "two cursors" symptom.Reproduction on a machine with a binary claude install:
Fix
In the binary branch of
runClaudeCli:SIGINT/SIGTERM/SIGHUP/SIGQUITfrom launcher to the spawned child.'exit', sendSIGTERMto the child (covers natural exit).ppidWatchthat fires when the launcher'sprocess.ppid === 1(covers ungraceful parent death where no signal is propagated). Interval isunref()'d so it doesn't keep the event loop alive after normal child exit.21 lines added, no behavior change for the existing happy path.
Verified locally
Synthetic launcher mirroring
runClaudeCli's spawn pattern, with a long-lived/bin/sleepas the fake child:kill -TERMlauncherkill -TERMlauncherkill -HUPlauncher (terminal-close case)kill -KILLlauncher's parent shellNotes
.js/.cjsimport-path branch, since orphans are impossible there.prctl(PR_SET_PDEATHSIG)via FFI for instant detection, but that's Linux-only and adds a native dep. The 1Hz polling is portable and adds negligible CPU.