farion1231/cc-switch · error · anyhow::Error
INVALID_SKILL_DIRECTORY
INVALID_SKILL_DIRECTORY
Error message
{"code":"INVALID_SKILL_DIRECTORY","context":{"directory":"{directory}"},"suggestion":"checkZipContent"} What it means
Structured error from CC Switch's skill installer: install() (src-tauri/src/services/skill.rs:774-781) rejects the skill when sanitize_skill_source_path(&skill.directory) returns None. That function (line 2758-2787) only accepts safe multi-segment RELATIVE paths: non-empty after trim, no RootDir/Prefix (absolute paths like /x or C:\x rejected), and no '.', '..', or empty segments. The JSON payload (format_skill_error, src-tauri/src/error.rs) carries code INVALID_SKILL_DIRECTORY, context.directory = the raw skill.directory, and suggestion 'checkZipContent'.
Source
Thrown at src-tauri/src/services/skill.rs:775
}
/// 安装 Skill
///
/// 流程:
/// 1. 下载到 SSOT 目录
/// 2. 保存到数据库
/// 3. 同步到启用的应用目录
pub async fn install(
&self,
db: &Arc<Database>,
skill: &DiscoverableSkill,
current_app: &AppType,
) -> Result<InstalledSkill> {
let ssot_dir = Self::get_ssot_dir()?;
// 允许多级目录(如 a/b/c),但必须是安全的相对路径。
let source_rel = Self::sanitize_skill_source_path(&skill.directory).ok_or_else(|| {
anyhow!(format_skill_error(
"INVALID_SKILL_DIRECTORY",
&[("directory", &skill.directory)],
Some("checkZipContent"),
))
})?;
// 安装目录名始终使用最后一段,避免在 SSOT 中创建多级目录。
let install_name = source_rel
.file_name()
.and_then(|name| Self::sanitize_install_name(&name.to_string_lossy()))
.ok_or_else(|| {
anyhow!(format_skill_error(
"INVALID_SKILL_DIRECTORY",
&[("directory", &skill.directory)],
Some("checkZipContent"),
))
})?;
// Fast path for an existing installation. The write guard makes the DBView on GitHub (pinned to a2e22f3302)
Solutions
- Correct skill.directory to a safe relative path such as 'pdf' or 'catalog/pdf' and retry
- Refresh the skills.sh index / re-fetch repo skills so the directory field is repopulated
- If curating your own marketplace, validate entries with the same rule: non-empty relative path, no '.'/'..' or absolute components
Example fix
// before
{ "directory": "/skills/pdf" }
// after
{ "directory": "skills/pdf" } Defensive patterns
Strategy: validation
Validate before calling
function isSafeRelativeDirectory(raw: string): boolean {
const t = raw.trim();
if (t === "" || t.startsWith("/") || /^[A-Za-z]:[\\/]/.test(t)) return false;
return t.split("/").every((seg) => {
const s = seg.trim();
return s !== "" && s !== "." && s !== "..";
});
}
if (!isSafeRelativeDirectory(skill.directory)) blockInstall(); Type guard
function isSafeRelativeDirectory(raw: string): raw is string {
/* same body as validationCode; true only for non-empty relative paths
without '.', '..', or empty segments */
} Try / catch
try {
await install(skill);
} catch (e) {
if (isSkillError(e) && e.code === "INVALID_SKILL_DIRECTORY") {
reportBadCatalogEntry(e.context.directory, e.suggestion /* checkZipContent */);
return;
}
throw e;
} Prevention
- Treat the skills.sh directory field as untrusted input; always relative, never absolute
- Validate catalog payloads before caching them
- Reject traversal strings ('..', './') at data-entry time
When it happens
Trigger: A DiscoverableSkill whose directory is '', ' ', '/etc/passwd', '../../repo', './skill', 'C:\\skills\\x', or a path whose segments normalize to empty; typically fed from the skills.sh index or a hand-crafted marketplace payload.
Common situations: Corrupted/stale skills.sh catalog entries; hand-edited repo metadata where directory became absolute; security probing with traversal strings; snapshot import writing malformed rows into the skills table.
Related errors
- SKILL_DIRECTORY_CONFLICT
- SKILL_DIR_NOT_FOUND
- Invalid skill directory (possible path traversal): {director
- Invalid backup id: {backup_id}
- OPENCLAW_ENV_EMPTY
AI-assisted analysis of farion1231/cc-switch@a2e22f3302 (2026-08-16).
Data as JSON: /api/errors/4be75e3a6037f7e4.
Report an issue: GitHub.