From 2d478876fa35463d6a6413a537eebd1e46347f9e Mon Sep 17 00:00:00 2001 From: makoMakoGo <48956204+makoMakoGo@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:00:56 +0800 Subject: [PATCH] fix: align Claude MCP path for custom config dirs (#3431) * fix claude mcp path for custom config dir * fix claude mcp custom profile isolation * fix claude mcp override path edges * fix ci test isolation --- src-tauri/src/claude_desktop_config.rs | 6 +- src-tauri/src/claude_mcp.rs | 44 +--- src-tauri/src/config.rs | 185 ++++++++++++-- src-tauri/src/database/dao/usage_rollup.rs | 6 +- src-tauri/src/services/provider/mod.rs | 5 +- src-tauri/src/services/proxy.rs | 12 +- src-tauri/tests/mcp_commands.rs | 273 ++++++++++++++++++++- src-tauri/tests/provider_service.rs | 16 +- src-tauri/tests/support.rs | 1 + src/i18n/locales/en.json | 2 +- src/i18n/locales/ja.json | 2 +- src/i18n/locales/zh-TW.json | 2 +- src/i18n/locales/zh.json | 2 +- 13 files changed, 470 insertions(+), 86 deletions(-) diff --git a/src-tauri/src/claude_desktop_config.rs b/src-tauri/src/claude_desktop_config.rs index 0824aedf1..8b3de69f2 100644 --- a/src-tauri/src/claude_desktop_config.rs +++ b/src-tauri/src/claude_desktop_config.rs @@ -1339,8 +1339,10 @@ mod tests { } fn set_proxy_port(db: &Database, port: u16) { - let mut config = crate::proxy::types::ProxyConfig::default(); - config.listen_port = port; + let config = crate::proxy::types::ProxyConfig { + listen_port: port, + ..Default::default() + }; futures::executor::block_on(db.update_proxy_config(config)).expect("update proxy config"); } diff --git a/src-tauri/src/claude_mcp.rs b/src-tauri/src/claude_mcp.rs index c35a05f27..9da3f63fd 100644 --- a/src-tauri/src/claude_mcp.rs +++ b/src-tauri/src/claude_mcp.rs @@ -4,7 +4,7 @@ use std::env; use std::fs; use std::path::{Path, PathBuf}; -use crate::config::{atomic_write, get_claude_mcp_path, get_default_claude_mcp_path}; +use crate::config::{atomic_write, get_claude_mcp_path}; use crate::error::AppError; /// 需要在 Windows 上用 cmd /c 包装的命令 @@ -98,51 +98,9 @@ pub struct McpStatus { } fn user_config_path() -> PathBuf { - ensure_mcp_override_migrated(); get_claude_mcp_path() } -fn ensure_mcp_override_migrated() { - if crate::settings::get_claude_override_dir().is_none() { - return; - } - - let new_path = get_claude_mcp_path(); - if new_path.exists() { - return; - } - - let legacy_path = get_default_claude_mcp_path(); - if !legacy_path.exists() { - return; - } - - if let Some(parent) = new_path.parent() { - if let Err(err) = fs::create_dir_all(parent) { - log::warn!("创建 MCP 目录失败: {err}"); - return; - } - } - - match fs::copy(&legacy_path, &new_path) { - Ok(_) => { - log::info!( - "已根据覆盖目录复制 MCP 配置: {} -> {}", - legacy_path.display(), - new_path.display() - ); - } - Err(err) => { - log::warn!( - "复制 MCP 配置失败: {} -> {}: {}", - legacy_path.display(), - new_path.display(), - err - ); - } - } -} - fn read_json_value(path: &Path) -> Result { if !path.exists() { return Ok(serde_json::json!({})); diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 085ca7724..ed4686522 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::fs; use std::io::Write; -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use crate::error::AppError; @@ -47,25 +47,118 @@ pub fn get_default_claude_mcp_path() -> PathBuf { get_home_dir().join(".claude.json") } -fn derive_mcp_path_from_override(dir: &Path) -> Option { - let file_name = dir - .file_name() - .map(|name| name.to_string_lossy().to_string())? - .trim() - .to_string(); - if file_name.is_empty() { - return None; +fn normalize_path_lexically(path: &Path) -> PathBuf { + let mut normalized = PathBuf::new(); + + for component in path.components() { + match component { + Component::CurDir => {} + Component::ParentDir => { + if !normalized.pop() { + normalized.push(component.as_os_str()); + } + } + Component::Normal(part) => normalized.push(part), + Component::RootDir | Component::Prefix(_) => normalized.push(component.as_os_str()), + } } - let parent = dir.parent().unwrap_or_else(|| Path::new("")); - Some(parent.join(format!("{file_name}.json"))) + + normalized } -/// 获取 Claude MCP 配置文件路径,若设置了目录覆盖则与覆盖目录同级 +fn comparable_path_key(path: &Path) -> String { + let mut key = normalize_path_lexically(path).to_string_lossy().to_string(); + + #[cfg(windows)] + { + key = key.replace('\\', "/"); + } + + while key.len() > 1 && key.ends_with('/') { + key.pop(); + } + + #[cfg(windows)] + { + key.make_ascii_lowercase(); + } + + key +} + +fn path_eq_lexical(left: &Path, right: &Path) -> bool { + comparable_path_key(left) == comparable_path_key(right) +} + +#[cfg(windows)] +fn derive_wsl_default_mcp_path(dir: &Path) -> Option { + use std::path::Prefix; + + let normalized = normalize_path_lexically(dir); + let mut components = normalized.components(); + let prefix = match components.next()? { + Component::Prefix(prefix) => prefix, + _ => return None, + }; + + let server = match prefix.kind() { + Prefix::UNC(server, _) | Prefix::VerbatimUNC(server, _) => server.to_string_lossy(), + _ => return None, + }; + + if !server.eq_ignore_ascii_case("wsl$") && !server.eq_ignore_ascii_case("wsl.localhost") { + return None; + } + + let mut parts = Vec::new(); + for component in components { + match component { + Component::RootDir | Component::CurDir => {} + Component::Normal(part) => parts.push(part.to_string_lossy().to_string()), + Component::ParentDir | Component::Prefix(_) => return None, + } + } + + let is_wsl_home_default = + parts.len() == 3 && parts[0] == "home" && !parts[1].is_empty() && parts[2] == ".claude"; + let is_wsl_root_default = parts.len() == 2 && parts[0] == "root" && parts[1] == ".claude"; + + if is_wsl_home_default || is_wsl_root_default { + return normalized + .parent() + .map(|parent| parent.join(".claude.json")); + } + + None +} + +fn default_mcp_path_for_config_dir(dir: &Path) -> Option { + let default_config_dir = get_home_dir().join(".claude"); + if path_eq_lexical(dir, &default_config_dir) { + return Some(get_default_claude_mcp_path()); + } + + #[cfg(windows)] + { + if let Some(path) = derive_wsl_default_mcp_path(dir) { + return Some(path); + } + } + + None +} + +fn derive_mcp_path_from_override(dir: &Path) -> PathBuf { + dir.join(".claude.json") +} + +/// 获取 Claude MCP 配置文件路径 pub fn get_claude_mcp_path() -> PathBuf { if let Some(custom_dir) = crate::settings::get_claude_override_dir() { - if let Some(path) = derive_mcp_path_from_override(&custom_dir) { + if let Some(path) = default_mcp_path_for_config_dir(&custom_dir) { return path; } + return derive_mcp_path_from_override(&custom_dir); } get_default_claude_mcp_path() } @@ -263,33 +356,73 @@ mod tests { use super::*; #[test] - fn derive_mcp_path_from_override_preserves_folder_name() { + fn derive_mcp_path_from_override_uses_config_dir_for_custom_path() { let override_dir = PathBuf::from("/tmp/profile/.claude"); - let derived = derive_mcp_path_from_override(&override_dir) - .expect("should derive path for nested dir"); - assert_eq!(derived, PathBuf::from("/tmp/profile/.claude.json")); + let derived = derive_mcp_path_from_override(&override_dir); + assert_eq!(derived, PathBuf::from("/tmp/profile/.claude/.claude.json")); } #[test] - fn derive_mcp_path_from_override_handles_non_hidden_folder() { + fn derive_mcp_path_from_override_uses_config_dir_for_non_hidden_folder() { let override_dir = PathBuf::from("/data/claude-config"); - let derived = derive_mcp_path_from_override(&override_dir) - .expect("should derive path for standard dir"); - assert_eq!(derived, PathBuf::from("/data/claude-config.json")); + let derived = derive_mcp_path_from_override(&override_dir); + assert_eq!(derived, PathBuf::from("/data/claude-config/.claude.json")); } #[test] fn derive_mcp_path_from_override_supports_relative_rootless_dir() { let override_dir = PathBuf::from("claude"); - let derived = derive_mcp_path_from_override(&override_dir) - .expect("should derive path for single segment"); - assert_eq!(derived, PathBuf::from("claude.json")); + let derived = derive_mcp_path_from_override(&override_dir); + assert_eq!(derived, PathBuf::from("claude/.claude.json")); } #[test] - fn derive_mcp_path_from_root_like_dir_returns_none() { + fn derive_mcp_path_from_root_like_dir_uses_root_file() { let override_dir = PathBuf::from("/"); - assert!(derive_mcp_path_from_override(&override_dir).is_none()); + let derived = derive_mcp_path_from_override(&override_dir); + assert_eq!(derived, PathBuf::from("/.claude.json")); + } + + #[test] + fn derive_mcp_path_from_override_preserves_leading_parent_dirs() { + let override_dir = PathBuf::from("../../profiles/work/.claude"); + let derived = derive_mcp_path_from_override(&override_dir); + assert_eq!(derived, override_dir.join(".claude.json")); + } + + #[cfg(windows)] + #[test] + fn wsl_unc_home_default_uses_split_mcp_path() { + let override_dir = PathBuf::from(r"\\wsl$\Ubuntu\home\travis\.claude"); + let derived = default_mcp_path_for_config_dir(&override_dir) + .expect("WSL home default should use split MCP path"); + assert_eq!( + derived, + PathBuf::from(r"\\wsl$\Ubuntu\home\travis\.claude.json") + ); + } + + #[cfg(windows)] + #[test] + fn wsl_unc_root_default_uses_split_mcp_path() { + let override_dir = PathBuf::from(r"\\wsl.localhost\Ubuntu\root\.claude"); + let derived = default_mcp_path_for_config_dir(&override_dir) + .expect("WSL root default should use split MCP path"); + assert_eq!( + derived, + PathBuf::from(r"\\wsl.localhost\Ubuntu\root\.claude.json") + ); + } + + #[cfg(windows)] + #[test] + fn wsl_unc_custom_dir_uses_nested_mcp_path() { + let override_dir = PathBuf::from(r"\\wsl$\Ubuntu\opt\claude\.claude"); + assert!(default_mcp_path_for_config_dir(&override_dir).is_none()); + assert_eq!( + derive_mcp_path_from_override(&override_dir), + PathBuf::from(r"\\wsl$\Ubuntu\opt\claude\.claude\.claude.json") + ); } #[test] diff --git a/src-tauri/src/database/dao/usage_rollup.rs b/src-tauri/src/database/dao/usage_rollup.rs index 3c2bc8994..eaa472798 100644 --- a/src-tauri/src/database/dao/usage_rollup.rs +++ b/src-tauri/src/database/dao/usage_rollup.rs @@ -493,8 +493,10 @@ mod tests { { let conn = crate::database::lock_conn!(db.conn); - let date_str = chrono::DateTime::from_timestamp(old_ts, 0) - .unwrap() + let date_str = Local + .timestamp_opt(old_ts, 0) + .single() + .expect("old timestamp should be a valid local datetime") .format("%Y-%m-%d") .to_string(); conn.execute( diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index d7eb15144..b51ed4835 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -463,6 +463,7 @@ base_url = "http://localhost:8080" db.update_proxy_config(ProxyConfig { live_takeover_active: true, + listen_port: 0, ..Default::default() }) .await @@ -491,7 +492,7 @@ base_url = "http://localhost:8080" ) .expect("seed taken-over live file"); - state + let proxy_info = state .proxy_service .start() .await @@ -544,7 +545,7 @@ base_url = "http://localhost:8080" live.get("env") .and_then(|env| env.get("ANTHROPIC_BASE_URL")) .and_then(|v| v.as_str()), - Some("http://127.0.0.1:15721"), + Some(format!("http://127.0.0.1:{}", proxy_info.port).as_str()), "proxy base URL should stay intact" ); assert!( diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index b99bbf5a4..7327073f6 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -5444,8 +5444,10 @@ command = "shared-command" ) .expect("set common config snippet"); - let mut proxy_config = ProxyConfig::default(); - proxy_config.listen_port = 0; + let proxy_config = ProxyConfig { + listen_port: 0, + ..Default::default() + }; db.update_proxy_config(proxy_config) .await .expect("set test proxy config"); @@ -5582,8 +5584,10 @@ requires_openai_auth = true let db = Arc::new(Database::memory().expect("init db")); let state = crate::store::AppState::new(db.clone()); - let mut proxy_config = ProxyConfig::default(); - proxy_config.listen_port = 0; + let proxy_config = ProxyConfig { + listen_port: 0, + ..Default::default() + }; db.update_proxy_config(proxy_config) .await .expect("set test proxy config"); diff --git a/src-tauri/tests/mcp_commands.rs b/src-tauri/tests/mcp_commands.rs index 242b6e835..3bcf29e6a 100644 --- a/src-tauri/tests/mcp_commands.rs +++ b/src-tauri/tests/mcp_commands.rs @@ -4,8 +4,9 @@ use std::fs; use serde_json::json; use cc_switch_lib::{ - get_claude_mcp_path, get_claude_settings_path, import_default_config_test_hook, AppError, - AppType, McpApps, McpServer, McpService, MultiAppConfig, + get_claude_mcp_path, get_claude_mcp_status, get_claude_settings_path, + import_default_config_test_hook, read_claude_mcp_config, update_settings, AppError, + AppSettings, AppType, McpApps, McpServer, McpService, MultiAppConfig, }; #[path = "support.rs"] @@ -673,6 +674,274 @@ fn enabling_claude_mcp_skips_when_claude_config_absent() { ); } +#[test] +fn explicit_default_claude_dir_keeps_default_split_mcp_path() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let home = ensure_test_home(); + let claude_dir = home.join(".claude"); + fs::create_dir_all(&claude_dir).expect("create explicit default claude dir"); + + update_settings(AppSettings { + claude_config_dir: Some(claude_dir.to_string_lossy().to_string()), + ..AppSettings::default() + }) + .expect("set explicit default claude config dir"); + + assert_eq!( + get_claude_mcp_path(), + home.join(".claude.json"), + "explicit default Claude dir should keep Claude Code's split MCP path" + ); + + let state = create_test_state().expect("create test state"); + McpService::upsert_server( + &state, + McpServer { + id: "claude-default".to_string(), + name: "Claude Default".to_string(), + server: json!({ + "type": "stdio", + "command": "echo" + }), + apps: McpApps { + claude: true, + codex: false, + gemini: false, + opencode: false, + hermes: false, + }, + description: None, + homepage: None, + docs: None, + tags: Vec::new(), + }, + ) + .expect("sync default Claude MCP"); + + assert!( + home.join(".claude.json").exists(), + "default split MCP file should be written at home/.claude.json" + ); + assert!( + !claude_dir.join(".claude.json").exists(), + "explicit default dir should not use nested .claude/.claude.json" + ); +} + +#[test] +fn custom_claude_dir_writes_mcp_inside_config_dir() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let home = ensure_test_home(); + let custom_dir = home.join("profiles").join(".claude"); + fs::create_dir_all(&custom_dir).expect("create custom claude dir"); + + update_settings(AppSettings { + claude_config_dir: Some(custom_dir.to_string_lossy().to_string()), + ..AppSettings::default() + }) + .expect("set custom claude config dir"); + + let expected_mcp_path = custom_dir.join(".claude.json"); + assert_eq!( + get_claude_mcp_path(), + expected_mcp_path, + "custom Claude dir should keep MCP state inside the config dir" + ); + + let state = create_test_state().expect("create test state"); + McpService::upsert_server( + &state, + McpServer { + id: "claude-custom".to_string(), + name: "Claude Custom".to_string(), + server: json!({ + "type": "stdio", + "command": "echo" + }), + apps: McpApps { + claude: true, + codex: false, + gemini: false, + opencode: false, + hermes: false, + }, + description: None, + homepage: None, + docs: None, + tags: Vec::new(), + }, + ) + .expect("sync custom Claude MCP"); + + assert!( + expected_mcp_path.exists(), + "custom Claude MCP file should be written inside custom dir" + ); + assert!( + !home.join("profiles").join(".claude.json").exists(), + "custom Claude dir should not write sibling .claude.json" + ); +} + +#[test] +fn custom_claude_dir_sync_does_not_copy_default_profile() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let home = ensure_test_home(); + let home_mcp_path = home.join(".claude.json"); + let default_profile = json!({ + "hasCompletedOnboarding": true, + "projects": { + "/home-project": { + "hasTrustDialogAccepted": true + } + }, + "mcpServers": { + "home-only": { + "type": "stdio", + "command": "home-command" + } + }, + "profileSentinel": "home-profile" + }); + let default_profile_text = + serde_json::to_string_pretty(&default_profile).expect("serialize default profile"); + fs::write(&home_mcp_path, &default_profile_text).expect("seed default Claude profile"); + + let custom_dir = home.join("profiles").join("work").join(".claude"); + fs::create_dir_all(&custom_dir).expect("create custom claude dir"); + update_settings(AppSettings { + claude_config_dir: Some(custom_dir.to_string_lossy().to_string()), + ..AppSettings::default() + }) + .expect("set custom claude config dir"); + + let expected_mcp_path = custom_dir.join(".claude.json"); + assert_eq!( + get_claude_mcp_path(), + expected_mcp_path, + "custom Claude dir should use nested .claude.json" + ); + assert!( + !expected_mcp_path.exists(), + "custom profile should start without a live MCP file" + ); + + let state = create_test_state().expect("create test state"); + McpService::upsert_server( + &state, + McpServer { + id: "custom-only".to_string(), + name: "Custom Only".to_string(), + server: json!({ + "type": "stdio", + "command": "custom-command" + }), + apps: McpApps { + claude: true, + codex: false, + gemini: false, + opencode: false, + hermes: false, + }, + description: None, + homepage: None, + docs: None, + tags: Vec::new(), + }, + ) + .expect("sync custom Claude MCP"); + + let text = fs::read_to_string(&expected_mcp_path).expect("read custom Claude MCP"); + let value: serde_json::Value = serde_json::from_str(&text).expect("parse custom Claude MCP"); + let servers = value + .get("mcpServers") + .and_then(|v| v.as_object()) + .expect("custom profile should contain mcpServers"); + assert!( + servers.contains_key("custom-only"), + "custom profile should contain DB-managed Claude server" + ); + assert!( + !servers.contains_key("home-only"), + "custom profile should not inherit default profile MCP servers" + ); + assert!( + value.get("hasCompletedOnboarding").is_none(), + "custom profile should not inherit onboarding state" + ); + assert!( + value.get("projects").is_none(), + "custom profile should not inherit project trust state" + ); + assert!( + value.get("profileSentinel").is_none(), + "custom profile should not inherit unrelated default profile fields" + ); + assert_eq!( + fs::read_to_string(&home_mcp_path).expect("reread default Claude profile"), + default_profile_text, + "default Claude profile should remain unchanged" + ); +} + +#[test] +fn custom_claude_dir_read_only_mcp_queries_do_not_create_profile() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let home = ensure_test_home(); + let home_mcp_path = home.join(".claude.json"); + fs::write( + &home_mcp_path, + serde_json::to_string_pretty(&json!({ + "mcpServers": { + "home-only": { + "type": "stdio", + "command": "home-command" + } + }, + "profileSentinel": "home-profile" + })) + .expect("serialize default profile"), + ) + .expect("seed default Claude profile"); + + let custom_dir = home.join("profiles").join("work").join(".claude"); + fs::create_dir_all(&custom_dir).expect("create custom claude dir"); + update_settings(AppSettings { + claude_config_dir: Some(custom_dir.to_string_lossy().to_string()), + ..AppSettings::default() + }) + .expect("set custom claude config dir"); + + let expected_mcp_path = custom_dir.join(".claude.json"); + assert!( + !expected_mcp_path.exists(), + "custom profile should start without a live MCP file" + ); + + let status = + futures::executor::block_on(get_claude_mcp_status()).expect("get Claude MCP status"); + assert_eq!( + status.user_config_path, + expected_mcp_path.to_string_lossy(), + "status should report the custom profile MCP path" + ); + assert!( + !status.user_config_exists, + "status should report missing custom profile MCP file" + ); + let text = + futures::executor::block_on(read_claude_mcp_config()).expect("read Claude MCP config"); + assert_eq!(text, None, "missing custom profile should read as None"); + assert!( + !expected_mcp_path.exists(), + "read-only MCP queries should not copy or create the custom profile" + ); +} + #[test] fn sync_all_enabled_removes_known_disabled_but_preserves_unknown_live_entries() { let _guard = test_mutex().lock().expect("acquire test mutex"); diff --git a/src-tauri/tests/provider_service.rs b/src-tauri/tests/provider_service.rs index 5d4eb51a2..baea330a6 100644 --- a/src-tauri/tests/provider_service.rs +++ b/src-tauri/tests/provider_service.rs @@ -597,6 +597,14 @@ wire_api = "responses" let state = create_test_state_with_config(&initial_config).expect("create test state"); + let mut proxy_config = state.db.get_proxy_config().await.expect("get proxy config"); + proxy_config.listen_port = 0; + state + .db + .update_proxy_config(proxy_config) + .await + .expect("use ephemeral proxy port"); + ProviderService::switch(&state, AppType::Codex, "deepseek-provider") .expect("switch from official subscription to DeepSeek"); @@ -623,6 +631,12 @@ wire_api = "responses" .set_takeover_for_app("codex", true) .await .expect("enable Codex takeover"); + let proxy_status = state + .proxy_service + .get_status() + .await + .expect("read proxy status after takeover"); + let codex_proxy_base_url = format!("http://127.0.0.1:{}/v1", proxy_status.port); let auth_after_takeover: serde_json::Value = read_json_file(&cc_switch_lib::get_codex_auth_path()).expect("read auth after takeover"); @@ -634,7 +648,7 @@ wire_api = "responses" let config_after_takeover = std::fs::read_to_string(cc_switch_lib::get_codex_config_path()).expect("read config"); assert!( - config_after_takeover.contains("http://127.0.0.1:15721/v1"), + config_after_takeover.contains(&codex_proxy_base_url), "enabling takeover should point Codex config.toml at the local proxy" ); assert!( diff --git a/src-tauri/tests/support.rs b/src-tauri/tests/support.rs index 3fd9602b7..35ff1ee3a 100644 --- a/src-tauri/tests/support.rs +++ b/src-tauri/tests/support.rs @@ -33,6 +33,7 @@ pub fn reset_test_fs() { ".gemini", ".config", ".openclaw", + "profiles", ] { let path = home.join(sub); if path.exists() { diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 2e80bd705..e8ebc92b5 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -754,7 +754,7 @@ "appConfigDirDescription": "Customize the storage location for CC Switch configuration (point to cloud sync folder to enable config sync)", "browsePlaceholderApp": "e.g., C:\\Users\\Administrator\\.cc-switch", "claudeConfigDir": "Claude Code Configuration Directory", - "claudeConfigDirDescription": "Override Claude configuration directory (settings.json) and keep claude.json (MCP) alongside it.", + "claudeConfigDirDescription": "Override Claude configuration directory (settings.json). Custom directories store .claude.json (MCP) inside the directory; the default ~/.claude directory keeps Claude Code's split ~/.claude.json path.", "codexConfigDir": "Codex Configuration Directory", "codexConfigDirDescription": "Override Codex configuration directory.", "geminiConfigDir": "Gemini Configuration Directory", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index bfefcad50..7ea435958 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -754,7 +754,7 @@ "appConfigDirDescription": "CC Switch の保存場所をカスタマイズします(クラウド同期フォルダを指定すると設定を同期できます)", "browsePlaceholderApp": "例: C:\\\\Users\\\\Administrator\\\\.cc-switch", "claudeConfigDir": "Claude Code 設定ディレクトリ", - "claudeConfigDirDescription": "Claude の設定ディレクトリ(settings.json)を上書きし、claude.json(MCP)も同じ場所に置きます。", + "claudeConfigDirDescription": "Claude の設定ディレクトリ(settings.json)を上書きします。カスタムディレクトリでは .claude.json(MCP)をその中に保存し、既定の ~/.claude では Claude Code の既定どおり ~/.claude.json を使います。", "codexConfigDir": "Codex 設定ディレクトリ", "codexConfigDirDescription": "Codex の設定ディレクトリを上書きします。", "geminiConfigDir": "Gemini 設定ディレクトリ", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index ef2bb63c9..5d3022567 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -754,7 +754,7 @@ "appConfigDirDescription": "自訂 CC Switch 的設定儲存位置(指定至雲端同步資料夾即可雲端同步設定)", "browsePlaceholderApp": "例如:C:\\Users\\Administrator\\.cc-switch", "claudeConfigDir": "Claude Code 設定目錄", - "claudeConfigDirDescription": "覆寫 Claude 設定目錄 (settings.json),同時會在同級存放 Claude MCP 的 claude.json。", + "claudeConfigDirDescription": "覆寫 Claude 設定目錄 (settings.json)。真正自訂目錄會在目錄內存放 Claude MCP 的 .claude.json;預設 ~/.claude 目錄仍使用 Claude 預設的 ~/.claude.json。", "codexConfigDir": "Codex 設定目錄", "codexConfigDirDescription": "覆寫 Codex 設定目錄。", "geminiConfigDir": "Gemini 設定目錄", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index d52bb694b..5722f436b 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -754,7 +754,7 @@ "appConfigDirDescription": "自定义 CC Switch 的配置存储位置(指定到云同步文件夹即可云同步配置)", "browsePlaceholderApp": "例如:C:\\Users\\Administrator\\.cc-switch", "claudeConfigDir": "Claude Code 配置目录", - "claudeConfigDirDescription": "覆盖 Claude 配置目录 (settings.json),同时会在同级存放 Claude MCP 的 claude.json。", + "claudeConfigDirDescription": "覆盖 Claude 配置目录 (settings.json)。真正自定义目录会在目录内存放 Claude MCP 的 .claude.json;默认 ~/.claude 目录仍使用 Claude 默认的 ~/.claude.json。", "codexConfigDir": "Codex 配置目录", "codexConfigDirDescription": "覆盖 Codex 配置目录。", "geminiConfigDir": "Gemini 配置目录",