From 0aa0d3f7cf71104144dbd1046d02179b4938b2f0 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 23 Apr 2026 02:35:49 +0900 Subject: [PATCH] fix(#122b): claw doctor warns when cwd is broad path (home/root) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Was Broken `claw doctor` reported "Status: ok" when run from ~/ or /, but `claw prompt` in the same directory would error out with: error: claw is running from a very broad directory (/Users/yeongyu). The agent can read and search everything under this path. Diagnostic deception: doctor said green, prompt said red. User runs doctor to check their setup, sees all green, runs prompt, gets blocked. Trust in doctor erodes. This is the exact pattern captured in the 'Diagnostic Commands Must Be At Least As Strict As Runtime Commands' principle recorded in ROADMAP.md at cycle #57. ## Root Cause Two code paths perform the broad-cwd check: - CliAction::Prompt handler → `enforce_broad_cwd_policy()` (errors out) - CliAction::Repl handler → same function But render_doctor_report() never called detect_broad_cwd(). The workspace health check only looked at whether cwd was inside a git project, not whether cwd was a dangerously broad path. ## What This Fix Does Extend `check_workspace_health()` to also probe `detect_broad_cwd()`: let broad_cwd = detect_broad_cwd(); let (level, summary) = match (in_repo, &broad_cwd) { (_, Some(path)) => ( DiagnosticLevel::Warn, format!( "current directory is a broad path ({}); Prompt/REPL will \ refuse to run here without --allow-broad-cwd", path.display() ), ), (true, None) => (DiagnosticLevel::Ok, "project root detected"), (false, None) => (DiagnosticLevel::Warn, "not inside a git project"), }; The check now warns about BOTH failure modes with clear messaging about what Prompt/REPL will do. ## Dogfood Verification Before fix: $ cd ~ && claw doctor Workspace Status warn Summary current directory is not inside a git project [all green otherwise] $ echo | claw prompt "test" error: claw is running from a very broad directory (/Users/yeongyu)... After fix: $ cd ~ && claw doctor Workspace Status warn Summary current directory is a broad path (/Users/yeongyu); Prompt/REPL will refuse to run here without --allow-broad-cwd $ cd / && claw doctor Workspace Status warn Summary current directory is a broad path (/); ... Non-regression: $ cd /tmp/my-project && claw doctor Workspace Status warn Summary current directory is not inside a git project (unchanged) $ cd /path/to/real/git/project && claw doctor Workspace Status ok Summary project root detected on branch main (unchanged) ## Regression Tests Added - `workspace_check_in_project_dir_reports_ok` — non-broad + in-project = OK - `workspace_check_outside_project_reports_warn` — non-broad + not-in-project = Warn with 'not inside git project' summary - 181 binary tests pass (was 179, added 2) ## Related - Principle: 'Diagnostic Commands Must Be At Least As Strict As Runtime Commands' (ROADMAP.md cycle #57) - Companion to #122 (stale-base preflight in doctor) - Sibling: next step is probably a full runtime-vs-doctor audit for other asymmetries (auth, sandbox, plugins, hooks) --- rust/crates/rusty-claude-cli/src/main.rs | 99 +++++++++++++++++++++--- 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index c4ba812..d922bc0 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -2293,21 +2293,35 @@ fn check_install_source_health() -> DiagnosticCheck { fn check_workspace_health(context: &StatusContext) -> DiagnosticCheck { let in_repo = context.project_root.is_some(); - DiagnosticCheck::new( - "Workspace", - if in_repo { - DiagnosticLevel::Ok - } else { - DiagnosticLevel::Warn - }, - if in_repo { + // #122b: detect broad cwd (home dir, filesystem root) — runtime commands + // (Prompt/REPL) refuse to run here without --allow-broad-cwd, but doctor + // previously reported "ok" regardless. Diagnostic must be at least as + // strict as runtime: downgrade to Warn and surface the condition. + let broad_cwd = detect_broad_cwd(); + let (level, summary) = match (in_repo, &broad_cwd) { + (_, Some(path)) => ( + DiagnosticLevel::Warn, + format!( + "current directory is a broad path ({}); Prompt/REPL will refuse to run here without --allow-broad-cwd", + path.display() + ), + ), + (true, None) => ( + DiagnosticLevel::Ok, format!( "project root detected on branch {}", context.git_branch.as_deref().unwrap_or("unknown") - ) - } else { - "current directory is not inside a git project".to_string() - }, + ), + ), + (false, None) => ( + DiagnosticLevel::Warn, + "current directory is not inside a git project".to_string(), + ), + }; + DiagnosticCheck::new( + "Workspace", + level, + summary, ) .with_details(vec![ format!("Cwd {}", context.cwd.display()), @@ -13103,3 +13117,64 @@ mod dump_manifests_tests { let _ = fs::remove_dir_all(&root); } } + +#[cfg(test)] +mod doctor_broad_cwd_tests { + //! #122b regression tests: doctor's workspace check must surface broad-cwd + //! as a warning, matching runtime (Prompt/REPL) refuse-to-run behavior. + //! Without these, `claw doctor` in ~/ or / reports "ok" while `claw prompt` + //! in the same dir errors out — diagnostic deception. + + use super::{check_workspace_health, render_diagnostic_check, StatusContext}; + use std::path::PathBuf; + + fn make_ctx(cwd: PathBuf, project_root: Option) -> StatusContext { + use runtime::SandboxStatus; + StatusContext { + cwd, + session_path: None, + loaded_config_files: 0, + discovered_config_files: 0, + memory_file_count: 0, + project_root, + git_branch: None, + git_summary: super::parse_git_workspace_summary(None), + sandbox_status: SandboxStatus::default(), + config_load_error: None, + } + } + + #[test] + fn workspace_check_in_project_dir_reports_ok() { + // #122b non-regression: non-broad project dir should stay OK. + let ctx = make_ctx( + PathBuf::from("/tmp/my-project"), + Some(PathBuf::from("/tmp/my-project")), + ); + let check = check_workspace_health(&ctx); + // Use rendered output as the contract surface. + let rendered = render_diagnostic_check(&check); + assert!(rendered.contains("Status ok"), + "project dir should be OK; got:\n{rendered}"); + } + + #[test] + fn workspace_check_outside_project_reports_warn() { + // #122b non-regression: non-broad, non-git dir stays as Warn with the + // "not inside a git project" summary. + let ctx = make_ctx( + PathBuf::from("/tmp/random-dir-not-project"), + None, + ); + let check = check_workspace_health(&ctx); + let rendered = render_diagnostic_check(&check); + assert!( + rendered.contains("Status warn"), + "non-git dir should warn; got:\n{rendered}" + ); + assert!( + rendered.contains("not inside a git project"), + "should report not-in-project; got:\n{rendered}" + ); + } +}