From bc31e53df63e18cfad4f3518514d1a24f60b81eb Mon Sep 17 00:00:00 2001 From: Sam Powers <35611153+sam-powers@users.noreply.github.com> Date: Sat, 13 Jun 2026 17:52:58 -0400 Subject: [PATCH] security: harden claude binary resolution Resolving the `claude` CLI path now validates every result before handing it to Command::new: - The login-shell fallback used an interactive login shell (`-lic`). Switch to a non-interactive login shell (`-lc`): we want the profile's PATH, not the side effects of interactive-only rc blocks. - The shell-resolved path and the `which` result are now both gated on `.is_file()`, matching the rigor the common-install-locations loop already applies. A stray line of shell output can no longer become the spawned binary. Co-Authored-By: Claude Opus 4.8 --- src-tauri/src/lib.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3bcd9d9..b67c113 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -995,8 +995,11 @@ fn resolve_claude_binary() -> Result { if let Ok(output) = Command::new("which").arg("claude").output() { if output.status.success() { let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if !path.is_empty() { - return Ok(PathBuf::from(path)); + let candidate = PathBuf::from(&path); + // Only trust the result if it actually names an existing file — a + // stray line of output should never be handed to Command::new. + if !path.is_empty() && candidate.is_file() { + return Ok(candidate); } } } @@ -1025,10 +1028,13 @@ fn resolve_claude_binary() -> Result { } } - // 3. Ask a login shell (sources the user's profile → full PATH). + // 3. Ask a login shell (sources the user's profile → full PATH). Use a + // non-interactive login shell (`-lc`, not `-lic`): we want the profile's + // PATH, not the side effects of interactive-only rc blocks, and `-c` + // keeps it to the single `command -v` we asked for. let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()); if let Ok(output) = Command::new(&shell) - .arg("-lic") + .arg("-lc") .arg("command -v claude") .output() { @@ -1041,8 +1047,11 @@ fn resolve_claude_binary() -> Result { .map(str::trim) .find(|l| !l.is_empty()) { - if !path.is_empty() { - return Ok(PathBuf::from(path)); + let candidate = PathBuf::from(path); + // Same guard as the other resolution paths: only return a real + // executable file, never an arbitrary line of shell output. + if candidate.is_file() { + return Ok(candidate); } } }