From e912f541f2e4b79d2309d39e587f92dff57aede9 Mon Sep 17 00:00:00 2001 From: neilmix Date: Sun, 1 Mar 2026 10:12:37 -0600 Subject: [PATCH] fix: treat EPERM from kill(pid, 0) as "process exists" in daemon liveness checks (#564) Per POSIX, kill(pid, 0) returns EPERM when the process exists but the caller lacks permission to signal it, and ESRCH when it does not exist. The daemon liveness checks in both the Rust CLI and TypeScript daemon treated any kill failure as "not running", which is incorrect when running inside a macOS sandbox that restricts signal delivery to (target self). This caused the CLI to delete the real daemon's socket and PID files, then spawn a duplicate daemon. Co-authored-by: Claude Opus 4.6 --- cli/src/connection.rs | 8 +++++++- cli/src/main.rs | 6 +++++- src/daemon.ts | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cli/src/connection.rs b/cli/src/connection.rs index 0079f85..dc4de8c 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -159,7 +159,13 @@ fn is_daemon_running(session: &str) -> bool { if let Ok(pid_str) = fs::read_to_string(&pid_path) { if let Ok(pid) = pid_str.trim().parse::() { unsafe { - return libc::kill(pid, 0) == 0; + if libc::kill(pid, 0) == 0 { + return true; + } + // EPERM means the process exists but we lack permission to + // signal it (e.g. inside a macOS sandbox). Only ESRCH means + // the process is genuinely gone. + return std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH); } } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 4df940e..c56a423 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -170,7 +170,11 @@ fn run_session(args: &[String], session: &str, json_mode: bool) { if let Ok(pid_str) = fs::read_to_string(&pid_path) { if let Ok(pid) = pid_str.trim().parse::() { #[cfg(unix)] - let running = unsafe { libc::kill(pid as i32, 0) == 0 }; + let running = unsafe { + libc::kill(pid as i32, 0) == 0 + || std::io::Error::last_os_error().raw_os_error() + != Some(libc::ESRCH) + }; #[cfg(windows)] let running = unsafe { let handle = diff --git a/src/daemon.ts b/src/daemon.ts index c47a7ff..c5faff4 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -262,7 +262,12 @@ export function isDaemonRunning(session?: string): boolean { // Check if process exists (works on both Unix and Windows) process.kill(pid, 0); return true; - } catch { + } catch (err: unknown) { + // EPERM means the process exists but we lack permission to signal it + // (e.g. caller is inside a macOS sandbox). Only ESRCH means it's gone. + if (err instanceof Error && (err as NodeJS.ErrnoException).code === 'EPERM') { + return true; + } // Process doesn't exist, clean up stale files cleanupSocket(session); return false;