From e043f4a16cf53cafc2a1acf1cc376f9859d8326c Mon Sep 17 00:00:00 2001 From: nux Date: Tue, 13 Jan 2026 11:42:16 -0800 Subject: [PATCH] feat(tmux): add KillSessionWithProcesses for explicit process termination Before calling tmux kill-session, explicitly kill the pane's process tree using pkill. This ensures claude processes don't survive session termination due to SIGHUP being caught/ignored. Implementation: - Add KillSessionWithProcesses() to tmux.go - Update killSessionsInOrder() in start.go to use new method - Update stopSession() in down.go to use new method Fixes: gt-5r7zr Co-Authored-By: Claude Opus 4.5 --- internal/cmd/down.go | 4 ++-- internal/cmd/start.go | 6 +++--- internal/tmux/tmux.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/internal/cmd/down.go b/internal/cmd/down.go index 0d9a2653..1851ce54 100644 --- a/internal/cmd/down.go +++ b/internal/cmd/down.go @@ -387,8 +387,8 @@ func stopSession(t *tmux.Tmux, sessionName string) (bool, error) { time.Sleep(100 * time.Millisecond) } - // Kill the session - return true, t.KillSession(sessionName) + // Kill the session (with explicit process termination to prevent orphans) + return true, t.KillSessionWithProcesses(sessionName) } // acquireShutdownLock prevents concurrent shutdowns. diff --git a/internal/cmd/start.go b/internal/cmd/start.go index cae239c8..7009b088 100644 --- a/internal/cmd/start.go +++ b/internal/cmd/start.go @@ -627,7 +627,7 @@ func killSessionsInOrder(t *tmux.Tmux, sessions []string, mayorSession, deaconSe // 1. Stop Deacon first if inList(deaconSession) { - if err := t.KillSession(deaconSession); err == nil { + if err := t.KillSessionWithProcesses(deaconSession); err == nil { fmt.Printf(" %s %s stopped\n", style.Bold.Render("✓"), deaconSession) stopped++ } @@ -638,7 +638,7 @@ func killSessionsInOrder(t *tmux.Tmux, sessions []string, mayorSession, deaconSe if sess == deaconSession || sess == mayorSession { continue } - if err := t.KillSession(sess); err == nil { + if err := t.KillSessionWithProcesses(sess); err == nil { fmt.Printf(" %s %s stopped\n", style.Bold.Render("✓"), sess) stopped++ } @@ -646,7 +646,7 @@ func killSessionsInOrder(t *tmux.Tmux, sessions []string, mayorSession, deaconSe // 3. Stop Mayor last if inList(mayorSession) { - if err := t.KillSession(mayorSession); err == nil { + if err := t.KillSessionWithProcesses(mayorSession); err == nil { fmt.Printf(" %s %s stopped\n", style.Bold.Render("✓"), mayorSession) stopped++ } diff --git a/internal/tmux/tmux.go b/internal/tmux/tmux.go index 117beacb..67e954f5 100644 --- a/internal/tmux/tmux.go +++ b/internal/tmux/tmux.go @@ -134,6 +134,39 @@ func (t *Tmux) KillSession(name string) error { return err } +// KillSessionWithProcesses explicitly kills all processes in a session before terminating it. +// This prevents orphaned processes that can occur when Claude catches/ignores SIGHUP. +// The kill sequence is: +// 1. Get the pane's PID +// 2. Send SIGTERM to the process group (all children) +// 3. Wait 100ms for graceful exit +// 4. Send SIGKILL if processes still alive +// 5. Kill the tmux session +func (t *Tmux) KillSessionWithProcesses(name string) error { + // Get the pane PID + pid, err := t.GetPanePID(name) + if err != nil { + // Session might not exist or be in bad state, try direct kill + return t.KillSession(name) + } + + if pid != "" { + // Send SIGTERM to process group (all children of the pane process) + termCmd := exec.Command("pkill", "-TERM", "-P", pid) + _ = termCmd.Run() // Ignore errors - process might already be dead + + // Wait for graceful termination + time.Sleep(100 * time.Millisecond) + + // Send SIGKILL to any survivors + killCmd := exec.Command("pkill", "-KILL", "-P", pid) + _ = killCmd.Run() // Ignore errors - process might already be dead + } + + // Now kill the tmux session + return t.KillSession(name) +} + // KillServer terminates the entire tmux server and all sessions. func (t *Tmux) KillServer() error { _, err := t.run("kill-server")