diff --git a/ROADMAP.md b/ROADMAP.md index c7160917..df296bc7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6338,7 +6338,7 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed) 420. **DONE — plugins help returns standard help envelope** — verified 2026-06-04: `plugins help` returns `{action:"help", kind:"plugin", usage:{...}}` matching mcp/agents/skills help shape. -421. **`status`, `mcp list`, `doctor` JSON output leak macOS `/private` symlink-canonicalized cwd instead of user-invocation cwd — automation that string-matches on cwd breaks across symlinked filesystems** — dogfooded 2026-05-11 by Jobdori on `b98b9a71` in response to Clawhip pinpoint nudge at `1503207549447573574`. Reproduction on macOS: invoke from `/tmp/claw-dog-cwd` (where `/tmp` symlinks to `/private/tmp`), then `claw status --output-format json` returns `workspace.cwd: "/private/tmp/claw-dog-cwd"`, `claw mcp list --output-format json` returns `working_directory: "/private/tmp/claw-dog-cwd"`. The user's invocation cwd (`$PWD`, `pwd`) is `/tmp/claw-dog-cwd`. Source: `session_control.rs:34` calls `fs::canonicalize(cwd)` for #151 cross-worktree session-bleed prevention, then leaks the canonicalized path through every JSON envelope that reports cwd. **Required fix shape:** (a) keep canonicalized cwd for session keying internally, but report user-input cwd (the value passed by `env::current_dir()` or `--cwd` flag) in JSON output as `cwd`; (b) optionally expose canonical path as a separate field `cwd_canonical` for diagnostic purposes; (c) audit every `--output-format json` surface that emits `cwd` / `working_directory` / `workspace.cwd` for the same leak (status, mcp list, doctor, session list, init, etc.); (d) add regression coverage proving JSON cwd matches `$PWD` on macOS where `/tmp -> /private/tmp` symlink exists. **Why this matters:** automation pipelines that route work to lanes by cwd, or that compare cwd against a registry, break across macOS hosts because the canonicalized form differs from the form the user/orchestrator passed. The leak is silent — no documentation indicates the path will be rewritten. Source: Jobdori live dogfood, `b98b9a71`, 2026-05-11. +421. **DONE — `status`, `mcp list`, `doctor` JSON output leak macOS `/private` symlink-canonicalized cwd instead of user-invocation cwd — automation that string-matches on cwd breaks across symlinked filesystems** — dogfooded 2026-05-11 by Jobdori on `b98b9a71` in response to Clawhip pinpoint nudge at `1503207549447573574`. Reproduction on macOS: invoke from `/tmp/claw-dog-cwd` (where `/tmp` symlinks to `/private/tmp`), then `claw status --output-format json` returns `workspace.cwd: "/private/tmp/claw-dog-cwd"`, `claw mcp list --output-format json` returns `working_directory: "/private/tmp/claw-dog-cwd"`. The user's invocation cwd (`$PWD`, `pwd`) is `/tmp/claw-dog-cwd`. Source: `session_control.rs:34` calls `fs::canonicalize(cwd)` for #151 cross-worktree session-bleed prevention, then leaks the canonicalized path through every JSON envelope that reports cwd. **Required fix shape:** (a) keep canonicalized cwd for session keying internally, but report user-input cwd (the value passed by `env::current_dir()` or `--cwd` flag) in JSON output as `cwd`; (b) optionally expose canonical path as a separate field `cwd_canonical` for diagnostic purposes; (c) audit every `--output-format json` surface that emits `cwd` / `working_directory` / `workspace.cwd` for the same leak (status, mcp list, doctor, session list, init, etc.); (d) add regression coverage proving JSON cwd matches `$PWD` on macOS where `/tmp -> /private/tmp` symlink exists. **Why this matters:** automation pipelines that route work to lanes by cwd, or that compare cwd against a registry, break across macOS hosts because the canonicalized form differs from the form the user/orchestrator passed. The leak is silent — no documentation indicates the path will be rewritten. Source: Jobdori live dogfood, `b98b9a71`, 2026-05-11. 422. **DONE — Unknown top-level subcommands fall through to chat prompt path instead of returning `unknown_subcommand` error — typos silently send the subcommand string as a chat message to the configured LLM** — dogfooded 2026-05-11 by Jobdori on `b98b9a71` in response to Clawhip pinpoint nudge at `1503215095088676956`. Reproduction: `unset ANTHROPIC_AUTH_TOKEN; export ANTHROPIC_API_KEY=fake-key-for-routing-test; claw completely-bogus-subcommand --output-format json` returns `{"error":"api returned 401 Unauthorized (authentication_error) [trace req_011...]: invalid x-api-key","kind":"api_http_error"}` — proving the unknown token reached the Anthropic API endpoint as a chat prompt. With valid credentials, the bogus subcommand string would be silently consumed as a chat message, billing the user for a typo and producing whatever continuation the LLM generates. **Pre-error path:** `claw --output-format json` with no creds returns `kind:"missing_credentials"` (the auth gate fires first), masking the routing bug. Only with creds present does the fallthrough manifest as the actual prompt being sent. **Sibling exit-code bug:** when the chat-path 401 returns, the JSON envelope is `kind:"api_http_error"` but exit code is **0**, while `cli_parse` errors (e.g. `--no-such-flag`) and `missing_credentials` errors correctly exit **1**. Exit-code parity between error envelopes is broken — automation that gates on `$?` will treat the 401-as-chat as success. **Required fix shape:** (a) reserve unknown top-level tokens that match no registered subcommand and emit `kind:"unknown_subcommand"` with `unknown:` field and exit code 1, BEFORE the chat fallback path; (b) when a token is intended as a chat prompt, require an explicit verb (`prompt`, `chat`, `ask`) or `--prompt` flag; (c) ensure exit codes are non-zero for all `kind:*_error` envelopes; (d) regression test: `claw --output-format json` with valid auth returns `kind:"unknown_subcommand"` exit 1, never reaches the API. **Why this matters:** automation that calls `claw ` with a programmatically constructed verb (typo, version drift, refactored command) silently bills tokens and produces hallucinated output instead of a typed error. Cross-cluster with #108 (CLI fallthrough discovered earlier) — #422 is the post-#108 audit confirming the routing bug still bites with valid credentials. Source: Jobdori live dogfood, `b98b9a71`, 2026-05-11. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 4a866f1c..c6ec805c 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -1121,7 +1121,7 @@ fn run() -> Result<(), Box> { println!("{}", render_diff_report()?); } CliOutputFormat::Json => { - let cwd = env::current_dir()?; + let cwd = friendly_cwd(env::current_dir()?); println!( "{}", serde_json::to_string_pretty(&render_diff_json_for(&cwd)?)? @@ -3608,7 +3608,7 @@ fn render_doctor_report( config_warning_mode: ConfigWarningMode, permission_mode: PermissionModeProvenance, ) -> Result> { - let cwd = env::current_dir()?; + let cwd = friendly_cwd(env::current_dir()?); let config_loader = ConfigLoader::default_for(&cwd); let config = load_config_with_warning_mode(&config_loader, config_warning_mode); let discovered_config = config_loader.discover(); @@ -9589,10 +9589,25 @@ fn status_json_value( }) } +/// #421: Strip macOS `/private` symlink prefix from paths so that +/// `status`, `doctor`, and `mcp list` JSON output matches the +/// user-visible invocation cwd instead of the canonicalized path. +fn friendly_cwd(path: PathBuf) -> PathBuf { + #[cfg(target_os = "macos")] + { + if let Ok(stripped) = path.strip_prefix("/private") { + if stripped.is_absolute() { + return stripped.to_path_buf(); + } + } + } + path +} + fn status_context( session_path: Option<&Path>, ) -> Result> { - let cwd = env::current_dir()?; + let cwd = friendly_cwd(env::current_dir()?); let loader = ConfigLoader::default_for(&cwd); // #456: count only paths that exist on disk, matching check_config_health behavior. let discovered_config_files = loader.discover().iter().filter(|e| e.path.exists()).count();