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

  1. 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.
  2. Remove symlinks between the SSOT dir and app skills dirs so both resolve to genuinely different locations.
  3. 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

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)