{"record":{"id":"002346839a3b760e","repo":"farion1231/cc-switch","slug":"skill-app-skills","errorCode":null,"errorMessage":"Skill 存储目录不能与 {app:?} 的 Skills 目录相同: {}","messagePattern":"Skill 存储目录不能与 (.+?) 的 Skills 目录相同: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/services/skill.rs","lineNumber":669,"sourceCode":"            if overlaps(&left, &right) {\n                return true;\n            }\n        }\n\n        // canonicalize() follows the final component and therefore fails for a\n        // dangling symlink. Resolve the parents separately so two applications\n        // cannot delete the same directory entry through aliased roots.\n        let canonical_entry =\n            |path: &Path| Some(path.parent()?.canonicalize().ok()?.join(path.file_name()?));\n        matches!(\n            (canonical_entry(left), canonical_entry(right)),\n            (Some(left), Some(right)) if overlaps(&left, &right)\n        )\n    }\n\n    fn ensure_distinct_skill_roots(ssot_dir: &Path, app_dir: &Path, app: &AppType) -> Result<()> {\n        if Self::paths_alias(ssot_dir, app_dir) {\n            return Err(anyhow!(\n                \"Skill 存储目录不能与 {app:?} 的 Skills 目录相同: {}\",\n                ssot_dir.display()\n            ));\n        }\n        Ok(())\n    }\n\n    fn get_distinct_app_skills_dir(ssot_dir: &Path, app: &AppType) -> Result<PathBuf> {\n        let app_dir = Self::get_app_skills_dir(app)?;\n        Self::ensure_distinct_skill_roots(ssot_dir, &app_dir, app)?;\n        Ok(app_dir)\n    }\n\n    fn validate_skill_storage_destination(ssot_dir: &Path) -> Result<()> {\n        for app in AppType::all() {\n            if matches!(app, AppType::ClaudeDesktop) {\n                continue;\n            }","sourceCodeStart":651,"sourceCodeEnd":687,"githubUrl":"https://github.com/farion1231/cc-switch/blob/a2e22f330273a5b6ffa87cb8b82b624601bac562/src-tauri/src/services/skill.rs#L651-L687","documentation":"Chinese-language guard in CC Switch's Rust skill service: ensure_distinct_skill_roots (src-tauri/src/services/skill.rs:667-675) rejects a configuration where the SSOT skill storage directory is the same path as the target app's Skills directory. 'Same' means literal equality or both paths canonicalizing to the same real path (paths_alias, line 631-640), which catches symlinks. The guard protects sync_to_app_dir from copying a directory into itself and two apps from deleting the same entries through aliased roots. Rendered message: 'Skill 存储目录不能与 {app} 的 Skills 目录相同: {ssot_dir}' (Skill storage directory must not equal the app's Skills directory).","triggerScenarios":"SkillStorageLocation::Unified resolves to ~/.agents/skills while an app override dir (e.g. claude_override_dir) is set to ~/.agents, making the app's skills dir identical to the SSOT dir; or a symlink from ~/.claude/skills pointing at ~/.cc-switch/skills; also hit from validate_skill_storage_destination which loops over all AppTypes when changing storage location.","commonSituations":"Users try to 'share' one folder between CC Switch and Claude/Codex to save disk; Pi agent dir configured to ~/.agents; migrating storage location while overrides from earlier experiments are still set in settings.json; symlink-based dotfile management.","solutions":["Change one side: pick a different skill storage location, or point the app override dir somewhere other than the SSOT root (e.g. keep Claude at ~/.claude instead of ~/.agents)","Remove symlinks between the SSOT directory and any app skills directory so canonical paths differ","Audit settings.json override keys (claude/codex/gemini/grok/opencode/openclaw/hermes) and the Pi agent dir after the error names the colliding app","Keep CC Switch's managed layout: SSOT under ~/.cc-switch/skills (or ~/.agents/skills) and let sync project into each app's own skills dir"],"exampleFix":"// before (settings.json — app skills dir aliases the unified SSOT dir)\n{ \"claudeOverrideDir\": \"~/.agents\" }\n// -> ~/.agents/skills == SSOT ~/.agents/skills -> error\n\n// after\n{ \"claudeOverrideDir\": \"~/.claude\" }","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match SkillService::get_distinct_app_skills_dir(&ssot_dir, &app) {\n    Ok(dir) => { /* proceed with sync */ }\n    Err(e) if e.to_string().contains(\"Skill 存储目录不能与\") => {\n        // storage location collides with the app's skills dir: fix settings, do not retry blindly\n        prompt_user_to_change_storage_location_or_override_dir();\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Never point an app override dir (claude/codex/gemini/...) at ~/.agents or ~/.cc-switch — its skills/ child then aliases the SSOT dir","Keep the managed layout: SSOT owns ~/.cc-switch/skills or ~/.agents/skills, apps own their own skills dirs","Avoid symlinking app skills dirs to the SSOT dir; canonicalization detects it","After changing skill storage location, run a sync once to surface collisions early"],"tags":["filesystem","skills","configuration","symlink","tauri"],"backgroundTag":null,"analyzedSha":"a2e22f330273a5b6ffa87cb8b82b624601bac562","analyzedAt":"2026-08-16T03:46:07.889Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}