farion1231/cc-switch · error · anyhow::Error

Skill already exists, please uninstall the current one first

Error message

Skill already exists, please uninstall the current one first: {}

What it means

restore_from_backup is insert-only: it refuses to run when metadata.skill.id is already in installed_skills, or when any installed skill occupies the same directory name (case-insensitive). Restoring over an existing skill could clobber live files, so the user must uninstall first.

Source

Thrown at src-tauri/src/services/skill.rs:1782

        let backup_path = Self::backup_path_for_id(backup_id)?;
        let metadata = Self::read_backup_metadata(&backup_path)?;
        let backup_skill_dir = backup_path.join("skill");
        if !backup_skill_dir.join("SKILL.md").exists() {
            return Err(anyhow!(
                "Skill backup is invalid or missing SKILL.md: {}",
                backup_path.display()
            ));
        }

        let existing_skills = db.get_all_installed_skills()?;
        if existing_skills.contains_key(&metadata.skill.id)
            || existing_skills.values().any(|skill| {
                skill
                    .directory
                    .eq_ignore_ascii_case(&metadata.skill.directory)
            })
        {
            return Err(anyhow!(
                "Skill already exists, please uninstall the current one first: {}",
                metadata.skill.directory
            ));
        }

        // meta.json 是文件内容(可能来自手工放置或不可信备份),directory 此前
        // 未经任何校验就直接 join——可穿越出 SSOT 目录写任意位置。必须先校验。
        let directory = Self::require_valid_directory(&metadata.skill.directory)?;

        let ssot_dir = Self::get_ssot_dir()?;
        let restore_path = ssot_dir.join(&directory);
        if restore_path.exists() || Self::is_symlink(&restore_path) {
            return Err(anyhow!(
                "Restore target already exists: {}",
                restore_path.display()
            ));
        }

View on GitHub (pinned to 0b5da51016)

Solutions

  1. Uninstall the existing skill named in the error message, then retry the restore
  2. If a different skill holds the same directory name, uninstall that one (or pick another backup)
  3. Check the installed list for case-variant directory collisions before restoring

Example fix

// before
await invoke('restore_skill_backup', { backupId, currentApp: 'claude' });

// after: clear the collision first
const skills = await invoke<Record<string, { id: string; directory: string }>>('get_all_installed_skills');
const collides = Object.values(skills).some(s =>
  s.id === backup.skillId || s.directory.toLowerCase() === backup.directory.toLowerCase());
if (collides) await invoke('uninstall_skill_unified', { id: backup.skillId });
await invoke('restore_skill_backup', { backupId, currentApp: 'claude' });
Defensive patterns

Strategy: validation

Validate before calling

const skills = await invoke<Record<string, { id: string; directory: string }>>('get_all_installed_skills');
const clash = Object.values(skills).some(s =>
  s.id === backupSkillId ||
  s.directory.toLowerCase() === backupDirectory.toLowerCase());
if (clash) {
  await invoke('uninstall_skill_unified', { id: backupSkillId });
}
await invoke('restore_skill_backup', { backupId, currentApp });

Try / catch

try {
  await invoke('restore_skill_backup', { backupId, currentApp });
} catch (e) {
  if (String(e).includes('already exists, please uninstall')) {
    notify('Uninstall the current skill first, then restore');
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Restoring a backup while the original skill is still installed; restoring after installing a different skill whose directory name matches the backup's (case differences count); duplicate ids from synced databases.

Common situations: Restore-before-uninstall workflows; name collisions after reinstalling a skill from a fork with the same directory name; backups restored onto machines that already have the skill.

Related errors


AI-assisted analysis of farion1231/cc-switch@0b5da51016 (2026-08-20). Data as JSON: /api/errors/eec79cb668b5317a. Report an issue: GitHub.