farion1231/cc-switch · critical · anyhow::Error
Skill 存储目录不能与 的 Skills 目录相同
Error message
Skill 存储目录不能与 {app:?} 的 Skills 目录相同: {} What it means
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.
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.
Example fix
# before (settings) skill_ssot_dir = "/home/user/.claude/skills" # same as Claude's Skills dir -> error # after skill_ssot_dir = "/home/user/.cc-switch/skills" # dedicated store; app dirs are projections
Defensive patterns
Strategy: validation
Validate before calling
// Rust: verify before any install/sync
fn distinct(a: &Path, b: &Path) -> bool {
let canon = |p: &Path| p.parent().and_then(|d| d.canonicalize().ok()).map(|d| d.join(p.file_name()?));
!matches!((canon(a), canon(b)), (Some(x), Some(y)) if x == y)
}
assert!(distinct(ssot_dir, &app_skills_dir(AppType::Claude)?)); Try / catch
if let Err(e) = SkillService::get_distinct_app_skills_dir(&ssot_dir, &app) {
let msg = e.to_string();
if msg.contains("Skills 目录相同") {
return Ok(prompt_user_to_change_ssot_dir()); // recoverable config error
}
return Err(e);
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of farion1231/cc-switch@06082e189d (2026-08-20).
Data as JSON: /api/errors/002346839a3b760e.
Report an issue: GitHub.
Appendix: source
Thrown at src-tauri/src/services/skill.rs:670
if overlaps(&left, &right) {
return true;
}
}
// canonicalize() follows the final component and therefore fails for a
// dangling symlink. Resolve the parents separately so two applications
// cannot delete the same directory entry through aliased roots.
let canonical_entry =
|path: &Path| Some(path.parent()?.canonicalize().ok()?.join(path.file_name()?));
matches!(
(canonical_entry(left), canonical_entry(right)),
(Some(left), Some(right)) if overlaps(&left, &right)
)
}
fn ensure_distinct_skill_roots(ssot_dir: &Path, app_dir: &Path, app: &AppType) -> Result<()> {
if Self::paths_alias(ssot_dir, app_dir) {
return Err(anyhow!(
"Skill 存储目录不能与 {app:?} 的 Skills 目录相同: {}",
ssot_dir.display()
));
}
Ok(())
}
fn get_distinct_app_skills_dir(ssot_dir: &Path, app: &AppType) -> Result<PathBuf> {
let app_dir = Self::get_app_skills_dir(app)?;
Self::ensure_distinct_skill_roots(ssot_dir, &app_dir, app)?;
Ok(app_dir)
}
fn validate_skill_storage_destination(ssot_dir: &Path) -> Result<()> {
for app in AppType::all() {
if matches!(app, AppType::ClaudeDesktop) {
continue;
}View on GitHub (pinned to 06082e189d)