diff --git a/hive-agent/src/web_ui/mod.rs b/hive-agent/src/web_ui/mod.rs index ebc3649e..a419fb54 100644 --- a/hive-agent/src/web_ui/mod.rs +++ b/hive-agent/src/web_ui/mod.rs @@ -291,13 +291,24 @@ enum SigintOutcome { /// process, if any. `hive-claude`'s driver spawns the turn's claude /// directly (`Command::new(program).spawn()`, no shell in between), so the /// harness is always the immediate parent of any claude turn it started — -/// scanning `/proc/*/status` for `Name: claude` + `PPid: ` -/// finds *that* specific process without needing the driver to surface its -/// pid through any extra plumbing. Distinguishes the harness's own tracked -/// turn from an unrelated `claude` someone is running interactively in the -/// same container (a manually shelled-in "choom" session) — that one's -/// parent is a login shell, not us. Best-effort: a process that exits -/// mid-scan (its `/proc//status` read fails, ESRCH) is just skipped. +/// scanning `/proc/*/status` for `PPid: ` plus `/proc/*/cmdline` +/// for an argv[0] of `claude` finds *that* specific process without needing +/// the driver to surface its pid through any extra plumbing. Distinguishes +/// the harness's own tracked turn from an unrelated `claude` someone is +/// running interactively in the same container (a manually shelled-in +/// "choom" session) — that one's parent is a login shell, not us. +/// +/// **Matches on `cmdline`, not `status`'s `Name:` field.** The nixpkgs +/// `claude-code` package wraps its real binary (`wrapProgram`-style: the +/// executable on `PATH` is a thin `exec -a claude .../.claude-wrapped ...` +/// shim) — `exec -a` only overrides argv[0] as the process itself/`cmdline` +/// see it, not the kernel's own `comm` (what `status`'s `Name:` line +/// reports, set from the executed binary's own basename at `execve` time). +/// So `Name:` shows `.claude-wrapped`, not `claude`, on a wrapped package — +/// `cmdline`'s first argument still carries the bare name +/// `Command::new("claude")` resolved on `PATH`, which is what actually +/// matters here. Best-effort: a process that exits mid-scan (its +/// `/proc//{status,cmdline}` read fails, ESRCH) is just skipped. fn find_claude_child() -> Option { let own_pid = std::process::id(); for entry in std::fs::read_dir("/proc").ok()?.flatten() { @@ -307,16 +318,18 @@ fn find_claude_child() -> Option { let Ok(status) = std::fs::read_to_string(entry.path().join("status")) else { continue; }; - let mut name = None; - let mut parent_pid = None; - for line in status.lines() { - if let Some(v) = line.strip_prefix("Name:") { - name = Some(v.trim()); - } else if let Some(v) = line.strip_prefix("PPid:") { - parent_pid = v.trim().parse::().ok(); - } + let parent_pid = status + .lines() + .find_map(|line| line.strip_prefix("PPid:")) + .and_then(|v| v.trim().parse::().ok()); + if parent_pid != Some(own_pid) { + continue; } - if name == Some("claude") && parent_pid == Some(own_pid) { + let Ok(cmdline) = std::fs::read(entry.path().join("cmdline")) else { + continue; + }; + let argv0 = cmdline.split(|&b| b == 0).next().unwrap_or_default(); + if argv0 == b"claude" { return Some(pid); } }