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
- Uninstall the existing skill named in the error message, then retry the restore
- If a different skill holds the same directory name, uninstall that one (or pick another backup)
- 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
- Always uninstall before restoring the same skill
- Check for case-insensitive directory-name collisions before restoring
- Remember restore is insert-only by design — it never overwrites
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
- Skill backup is invalid or missing SKILL.md: {}
- SKILL_DIRECTORY_CONFLICT
- Skill backup is not a directory: {}
- Restore target already exists: {}
- Pi 中已存在同名但内容不同的 Skill,拒绝覆盖或删除: {directory}
AI-assisted analysis of farion1231/cc-switch@0b5da51016 (2026-08-20).
Data as JSON: /api/errors/eec79cb668b5317a.
Report an issue: GitHub.