{"record":{"id":"002346839a3b760e","repo":"farion1231/cc-switch","slug":"skill-app-skills","errorCode":null,"errorMessage":"Skill 存储目录不能与 {app:?} 的 Skills 目录相同: {}","messagePattern":"Skill 存储目录不能与 (.+?) 的 Skills 目录相同: (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"critical","filePath":"src-tauri/src/services/skill.rs","lineNumber":670,"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":652,"sourceCodeEnd":688,"githubUrl":"https://github.com/farion1231/cc-switch/blob/06082e189d65e6d6dbadc35dacdac1ce6c79d89a/src-tauri/src/services/skill.rs#L652-L688","documentation":"The Tauri skill service keeps skills in one SSOT (single source of truth) directory and projects them into each app's Skills directory (e.g. Claude Code's). ensure_distinct_skill_roots rejects a configuration where the SSOT dir path-aliases the app dir, throwing this Chinese-language anyhow error that names the app and SSOT path. The alias check canonicalizes each path's parent (defeating dangling symlinks) then re-appends the file name and tests overlap, so symlinked or non-canonical spellings of the same directory are also caught — syncing onto itself would recurse or delete skill entries.","triggerScenarios":"Setting the skill storage (SSOT) directory in settings to the exact path of an app's Skills folder (e.g. ~/.claude/skills) or to a symlink/junction that resolves to it, then performing any install/sync that calls get_distinct_app_skills_dir.","commonSituations":"User points the skill store at ~/.claude/skills thinking it avoids duplication; macOS case-insensitive paths or trailing-slash/normalization differences; a symlink created to share one folder between tools; moving the SSOT into a directory the app also watches.","solutions":["Choose a dedicated SSOT directory that is not any app's Skills dir, e.g. ~/Skills-library or the app's default data dir, and retry the install.","Remove symlinks between the SSOT dir and app skills dirs so both resolve to genuinely different locations.","If you migrated directories, re-check the skill storage setting in the UI — saved paths are compared on every operation, so the old overlapping value must be changed."],"exampleFix":"# before (settings)\nskill_ssot_dir = \"/home/user/.claude/skills\"  # same as Claude's Skills dir -> error\n\n# after\nskill_ssot_dir = \"/home/user/.cc-switch/skills\"  # dedicated store; app dirs are projections","handlingStrategy":"validation","validationCode":"// Rust: verify before any install/sync\nfn distinct(a: &Path, b: &Path) -> bool {\n    let canon = |p: &Path| p.parent().and_then(|d| d.canonicalize().ok()).map(|d| d.join(p.file_name()?));\n    !matches!((canon(a), canon(b)), (Some(x), Some(y)) if x == y)\n}\nassert!(distinct(ssot_dir, &app_skills_dir(AppType::Claude)?));","typeGuard":null,"tryCatchPattern":"if let Err(e) = SkillService::get_distinct_app_skills_dir(&ssot_dir, &app) {\n    let msg = e.to_string();\n    if msg.contains(\"Skills 目录相同\") {\n        return Ok(prompt_user_to_change_ssot_dir()); // recoverable config error\n    }\n    return Err(e);\n}","preventionTips":["In the settings UI, reject SSOT directory choices that equal or symlink to any known app Skills directory.","Keep the SSOT store inside the app's own data directory and treat app dirs as read-only projections."],"tags":["skills","filesystem","path-conflict","tauri","configuration"],"backgroundTag":"path-directory-conflict","analyzedSha":"06082e189d65e6d6dbadc35dacdac1ce6c79d89a","analyzedAt":"2026-08-20T14:29:00.113Z","contentChangedAt":"2026-08-20T14:29:00.113Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}