{"record":{"id":"4be75e3a6037f7e4","repo":"farion1231/cc-switch","slug":"invalid-skill-directory","errorCode":"INVALID_SKILL_DIRECTORY","errorMessage":"{\"code\":\"INVALID_SKILL_DIRECTORY\",\"context\":{\"directory\":\"{directory}\"},\"suggestion\":\"checkZipContent\"}","messagePattern":"(.+?)\"\\},\"suggestion\":\"checkZipContent\"\\}","errorType":"error_code","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/services/skill.rs","lineNumber":775,"sourceCode":"    }\n\n    /// 安装 Skill\n    ///\n    /// 流程：\n    /// 1. 下载到 SSOT 目录\n    /// 2. 保存到数据库\n    /// 3. 同步到启用的应用目录\n    pub async fn install(\n        &self,\n        db: &Arc<Database>,\n        skill: &DiscoverableSkill,\n        current_app: &AppType,\n    ) -> Result<InstalledSkill> {\n        let ssot_dir = Self::get_ssot_dir()?;\n\n        // 允许多级目录（如 a/b/c），但必须是安全的相对路径。\n        let source_rel = Self::sanitize_skill_source_path(&skill.directory).ok_or_else(|| {\n            anyhow!(format_skill_error(\n                \"INVALID_SKILL_DIRECTORY\",\n                &[(\"directory\", &skill.directory)],\n                Some(\"checkZipContent\"),\n            ))\n        })?;\n        // 安装目录名始终使用最后一段，避免在 SSOT 中创建多级目录。\n        let install_name = source_rel\n            .file_name()\n            .and_then(|name| Self::sanitize_install_name(&name.to_string_lossy()))\n            .ok_or_else(|| {\n                anyhow!(format_skill_error(\n                    \"INVALID_SKILL_DIRECTORY\",\n                    &[(\"directory\", &skill.directory)],\n                    Some(\"checkZipContent\"),\n                ))\n            })?;\n\n        // Fast path for an existing installation. The write guard makes the DB","sourceCodeStart":757,"sourceCodeEnd":793,"githubUrl":"https://github.com/farion1231/cc-switch/blob/a2e22f330273a5b6ffa87cb8b82b624601bac562/src-tauri/src/services/skill.rs#L757-L793","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n{ \"directory\": \"/skills/pdf\" }\n\n// after\n{ \"directory\": \"skills/pdf\" }","handlingStrategy":"validation","validationCode":"function isSafeRelativeDirectory(raw: string): boolean {\n  const t = raw.trim();\n  if (t === \"\" || t.startsWith(\"/\") || /^[A-Za-z]:[\\\\/]/.test(t)) return false;\n  return t.split(\"/\").every((seg) => {\n    const s = seg.trim();\n    return s !== \"\" && s !== \".\" && s !== \"..\";\n  });\n}\nif (!isSafeRelativeDirectory(skill.directory)) blockInstall();","typeGuard":"function isSafeRelativeDirectory(raw: string): raw is string {\n  /* same body as validationCode; true only for non-empty relative paths\n     without '.', '..', or empty segments */\n}","tryCatchPattern":"try {\n  await install(skill);\n} catch (e) {\n  if (isSkillError(e) && e.code === \"INVALID_SKILL_DIRECTORY\") {\n    reportBadCatalogEntry(e.context.directory, e.suggestion /* checkZipContent */);\n    return;\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["skills","path-traversal","validation","install"],"backgroundTag":null,"analyzedSha":"a2e22f330273a5b6ffa87cb8b82b624601bac562","analyzedAt":"2026-08-16T03:46:07.889Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}