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 DB

View on GitHub (pinned to a2e22f3302)

Solutions

  1. Correct skill.directory to a safe relative path such as 'pdf' or 'catalog/pdf' and retry
  2. Refresh the skills.sh index / re-fetch repo skills so the directory field is repopulated
  3. 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

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


AI-assisted analysis of farion1231/cc-switch@a2e22f3302 (2026-08-16). Data as JSON: /api/errors/4be75e3a6037f7e4. Report an issue: GitHub.