farion1231/cc-switch · error · anyhow::Error
SKILL_DIRECTORY_CONFLICT
SKILL_DIRECTORY_CONFLICT
Error message
SKILL_DIRECTORY_CONFLICT
What it means
During skill install, the fast path reuse_existing_install finds an installed skill whose install_name (final directory segment) collides with the incoming one, but the existing record belongs to a different repository (owner/name mismatch), so it refuses to reuse or overwrite and returns a SKILL_DIRECTORY_CONFLICT error via format_skill_error. The payload is a JSON string with code, context (directory, existing_repo as 'owner/name'), and a suggestion key, which the frontend parses into a structured, actionable message. This protects the existing installation from being silently replaced by unrelated content that happens to share a directory name.
Solutions
- Uninstall the existing skill named in context.existing_repo first, then retry the install.
- Install the incoming skill from its original repo instead of the fork/second source so owner/name match and the reuse path applies.
- If both are wanted, change one skill's directory name at the source so install_name differs.
Example fix
# before
installed: { directory: "code-review", repo: "alice/skills" }
install({ directory: "code-review", repo_owner: "bob", repo_name: "skills", ... })
# -> SKILL_DIRECTORY_CONFLICT (directory=code-review, existing_repo=alice/skills)
# after
uninstall("code-review") # remove alice/skills version
install({ directory: "code-review", repo_owner: "bob", repo_name: "skills", ... }) # succeeds Defensive patterns
Strategy: try-catch
Validate before calling
// Before install, query for a same-name skill from another repo
let existing = db.find_skill_by_directory(&install_name)?;
if let Some(prev) = existing {
if prev.repo_owner != skill.repo_owner || prev.repo_name != skill.repo_name {
return plan_uninstall_first(&prev); // avoid the conflict deterministically
}
} Try / catch
match install(&db, &skill, &app).await {
Err(e) if e.to_string().contains("SKILL_DIRECTORY_CONFLICT") => {
let parsed: serde_json::Value = serde_json::from_str(&e.to_string())?;
let existing = parsed["context"]["existing_repo"].as_str().unwrap_or("unknown");
ui::confirm_uninstall(existing)?; // then retry install once
}
other => other?,
} Prevention
- Show 'already installed from <repo>' state in the skill list before users click install.
- When publishing skill repos, give folders unique names to avoid cross-repo install_name collisions.
When it happens
Trigger: Calling install() for a DiscoverableSkill from repo B whose directory field's last segment equals a skill already installed from repo A (e.g. both repos contain a top-level 'code-review' skill dir).
Common situations: Installing multiple skills collections that ship identically-named folders; a fork of a popular skills repo reinstalled under the fork's owner; renaming upstream changed the repo identity so the same logical skill now looks like a conflict.
Related errors
- INVALID_SKILL_DIRECTORY
- SKILL_DIR_NOT_FOUND
- DOWNLOAD_TIMEOUT
- Skill already exists, please uninstall the current one first
- Skill 存储目录不能与 的 Skills 目录相同
AI-assisted analysis of farion1231/cc-switch@06082e189d (2026-08-20).
Data as JSON: /api/errors/3bc7f51707c6f0b8.
Report an issue: GitHub.
Appendix: source
Thrown at src-tauri/src/services/skill.rs:736
continue;
}
let same_repo = existing.repo_owner.as_deref() == Some(&skill.repo_owner)
&& existing.repo_name.as_deref() == Some(&skill.repo_name);
if same_repo {
let mut updated = existing.clone();
updated.apps.set_enabled_for(current_app, true);
db.save_skill(&updated)?;
Self::sync_to_app_dir(&updated.directory, current_app)?;
log::info!(
"Skill {} 已存在,更新 {:?} 启用状态",
updated.name,
current_app
);
return Ok(Some(updated));
}
return Err(anyhow!(format_skill_error(
"SKILL_DIRECTORY_CONFLICT",
&[
("directory", install_name),
(
"existing_repo",
&format!(
"{}/{}",
existing.repo_owner.as_deref().unwrap_or("unknown"),
existing.repo_name.as_deref().unwrap_or("unknown")
)
),
(
"new_repo",
&format!("{}/{}", skill.repo_owner, skill.repo_name)
),
],
Some("uninstallFirst"),
)));View on GitHub (pinned to 06082e189d)