From f6f9cf7e9ff6ff40dbee3acc913da4fae715168a Mon Sep 17 00:00:00 2001 From: jaberjaber23 Date: Tue, 10 Mar 2026 02:14:49 +0300 Subject: [PATCH] fix claude-code --- crates/openfang-api/src/ws.rs | 7 +- .../src/drivers/claude_code.rs | 141 +++++++++++++----- 2 files changed, 109 insertions(+), 39 deletions(-) diff --git a/crates/openfang-api/src/ws.rs b/crates/openfang-api/src/ws.rs index 108eaf93..90025193 100644 --- a/crates/openfang-api/src/ws.rs +++ b/crates/openfang-api/src/ws.rs @@ -1143,7 +1143,12 @@ fn classify_streaming_error(err: &openfang_kernel::error::KernelError) -> String } } llm_errors::LlmErrorCategory::Format => { - "LLM request failed. Check your API key and model configuration in Settings.".to_string() + // Claude Code CLI errors have actionable messages — pass them through + if inner.contains("Claude Code CLI") || inner.contains("claude auth") { + classified.raw_message.clone() + } else { + "LLM request failed. Check your API key and model configuration in Settings.".to_string() + } } _ => classified.sanitized_message, } diff --git a/crates/openfang-runtime/src/drivers/claude_code.rs b/crates/openfang-runtime/src/drivers/claude_code.rs index 510e3dae..547ce343 100644 --- a/crates/openfang-runtime/src/drivers/claude_code.rs +++ b/crates/openfang-runtime/src/drivers/claude_code.rs @@ -12,13 +12,38 @@ use serde::Deserialize; use tokio::io::AsyncBufReadExt; use tracing::{debug, warn}; -/// Environment variables safe to pass through to the Claude Code subprocess. -/// XDG_* and CLAUDE_* prefixes are handled dynamically. -const SAFE_ENV_PREFIXES: &[&str] = &[ - "PATH", "HOME", "USER", "USERPROFILE", "TERM", "LANG", "SHELL", - "SYSTEMROOT", "PROGRAMFILES", "APPDATA", "LOCALAPPDATA", "TEMP", "TMP", +/// Environment variable names (and suffixes) to strip from the subprocess +/// to prevent leaking API keys from other providers. We keep the full env +/// intact (so Node.js, NVM, SSL, proxies, etc. all work) and only remove +/// secrets that belong to other LLM providers. +const SENSITIVE_ENV_EXACT: &[&str] = &[ + "OPENAI_API_KEY", + "ANTHROPIC_API_KEY", + "GEMINI_API_KEY", + "GOOGLE_API_KEY", + "GROQ_API_KEY", + "DEEPSEEK_API_KEY", + "MISTRAL_API_KEY", + "TOGETHER_API_KEY", + "FIREWORKS_API_KEY", + "OPENROUTER_API_KEY", + "PERPLEXITY_API_KEY", + "COHERE_API_KEY", + "AI21_API_KEY", + "CEREBRAS_API_KEY", + "SAMBANOVA_API_KEY", + "HUGGINGFACE_API_KEY", + "XAI_API_KEY", + "REPLICATE_API_TOKEN", + "BRAVE_API_KEY", + "TAVILY_API_KEY", + "ELEVENLABS_API_KEY", ]; +/// Suffixes that indicate a secret — remove any env var ending with these +/// unless it starts with `CLAUDE_`. +const SENSITIVE_SUFFIXES: &[&str] = &["_SECRET", "_TOKEN", "_PASSWORD"]; + /// LLM driver that delegates to the Claude Code CLI. pub struct ClaudeCodeDriver { cli_path: String, @@ -87,6 +112,30 @@ impl ClaudeCodeDriver { _ => Some(stripped.to_string()), } } + + /// Apply security env filtering to a command. + /// + /// Instead of `env_clear()` (which breaks Node.js, NVM, SSL, proxies), + /// we keep the full environment and only remove known sensitive API keys + /// from other LLM providers. + fn apply_env_filter(cmd: &mut tokio::process::Command) { + for key in SENSITIVE_ENV_EXACT { + cmd.env_remove(key); + } + // Remove any env var with a sensitive suffix, unless it's CLAUDE_* + for (key, _) in std::env::vars() { + if key.starts_with("CLAUDE_") { + continue; + } + let upper = key.to_uppercase(); + for suffix in SENSITIVE_SUFFIXES { + if upper.ends_with(suffix) { + cmd.env_remove(&key); + break; + } + } + } + } } /// JSON output from `claude -p --output-format json`. @@ -141,7 +190,6 @@ impl LlmDriver for ClaudeCodeDriver { let mut cmd = tokio::process::Command::new(&self.cli_path); cmd.arg("-p") .arg(&prompt) - .arg("--dangerously-skip-permissions") .arg("--output-format") .arg("json"); @@ -149,19 +197,7 @@ impl LlmDriver for ClaudeCodeDriver { cmd.arg("--model").arg(model); } - // SECURITY: Clear env and selectively re-add safe vars to prevent - // leaking API keys from other providers into the subprocess. - cmd.env_clear(); - for key in SAFE_ENV_PREFIXES { - if let Ok(val) = std::env::var(key) { - cmd.env(key, val); - } - } - for (key, val) in std::env::vars() { - if key.starts_with("XDG_") || key.starts_with("CLAUDE_") { - cmd.env(key, val); - } - } + Self::apply_env_filter(&mut cmd); cmd.stdout(std::process::Stdio::piped()); cmd.stderr(std::process::Stdio::piped()); @@ -171,13 +207,41 @@ impl LlmDriver for ClaudeCodeDriver { let output = cmd .output() .await - .map_err(|e| LlmError::Http(format!("Failed to spawn claude CLI: {e}")))?; + .map_err(|e| LlmError::Http(format!( + "Claude Code CLI not found or failed to start ({}). \ + Install: npm install -g @anthropic-ai/claude-code && claude auth", + e + )))?; if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let detail = if !stderr.is_empty() { &stderr } else { &stdout }; + let code = output.status.code().unwrap_or(1); + + // Provide actionable error messages + let message = if detail.contains("not authenticated") + || detail.contains("auth") + || detail.contains("login") + || detail.contains("credentials") + { + format!( + "Claude Code CLI is not authenticated. Run: claude auth\nDetail: {detail}" + ) + } else if detail.contains("permission") + || detail.contains("--dangerously-skip-permissions") + { + format!( + "Claude Code CLI requires permissions acceptance. \ + Run: claude --dangerously-skip-permissions (once to accept)\nDetail: {detail}" + ) + } else { + format!("Claude Code CLI exited with code {code}: {detail}") + }; + return Err(LlmError::Api { - status: output.status.code().unwrap_or(1) as u16, - message: format!("Claude CLI failed: {stderr}"), + status: code as u16, + message, }); } @@ -225,7 +289,6 @@ impl LlmDriver for ClaudeCodeDriver { let mut cmd = tokio::process::Command::new(&self.cli_path); cmd.arg("-p") .arg(&prompt) - .arg("--dangerously-skip-permissions") .arg("--output-format") .arg("stream-json") .arg("--verbose"); @@ -234,19 +297,7 @@ impl LlmDriver for ClaudeCodeDriver { cmd.arg("--model").arg(model); } - // SECURITY: Clear env and selectively re-add safe vars to prevent - // leaking API keys from other providers into the subprocess. - cmd.env_clear(); - for key in SAFE_ENV_PREFIXES { - if let Ok(val) = std::env::var(key) { - cmd.env(key, val); - } - } - for (key, val) in std::env::vars() { - if key.starts_with("XDG_") || key.starts_with("CLAUDE_") { - cmd.env(key, val); - } - } + Self::apply_env_filter(&mut cmd); cmd.stdout(std::process::Stdio::piped()); cmd.stderr(std::process::Stdio::piped()); @@ -255,7 +306,11 @@ impl LlmDriver for ClaudeCodeDriver { let mut child = cmd .spawn() - .map_err(|e| LlmError::Http(format!("Failed to spawn claude CLI: {e}")))?; + .map_err(|e| LlmError::Http(format!( + "Claude Code CLI not found or failed to start ({}). \ + Install: npm install -g @anthropic-ai/claude-code && claude auth", + e + )))?; let stdout = child .stdout @@ -455,4 +510,14 @@ mod tests { let driver = ClaudeCodeDriver::new(Some(String::new())); assert_eq!(driver.cli_path, "claude"); } + + #[test] + fn test_sensitive_env_list_coverage() { + // Ensure all major provider keys are in the strip list + assert!(SENSITIVE_ENV_EXACT.contains(&"OPENAI_API_KEY")); + assert!(SENSITIVE_ENV_EXACT.contains(&"ANTHROPIC_API_KEY")); + assert!(SENSITIVE_ENV_EXACT.contains(&"GEMINI_API_KEY")); + assert!(SENSITIVE_ENV_EXACT.contains(&"GROQ_API_KEY")); + assert!(SENSITIVE_ENV_EXACT.contains(&"DEEPSEEK_API_KEY")); + } }