siyuan-note/siyuan · error
unzip failed
Error message
unzip failed: %s
What it means
installFromZip writes the downloaded bytes to a temp zip and extracts it with gulu.Zip.Unzip; any extraction error is wrapped as 'unzip failed: <cause>'. gulu.Zip.Unzip also includes zip-slip protection, so an archive with entries escaping the target directory fails here too.
Solutions
- Verify the URL actually serves a zip file — open it in a browser; an HTML page means the URL is wrong or rate-limited
- Re-download and retry in case of a truncated/corrupted transfer
- Check the wrapped cause: a zip-slip message indicates a malicious or malformed archive — use a trusted source
- Check free disk space in the temp directory if the cause is a write error
Example fix
// before
InstallSkill("https://example.com/skill") // serves HTML, not a zip
// after
InstallSkill("https://example.com/skill.zip") Defensive patterns
Strategy: validation
Validate before calling
const r = await fetch(downloadUrl)
const ct = r.headers.get("content-type") || ""
const head = new Uint8Array(await (await r.blob()).slice(0, 4).arrayBuffer())
const isZip = ct.includes("zip") || (head[0] === 0x50 && head[1] === 0x4b)
if (!isZip) throw new Error("URL does not serve a zip file") Type guard
const looksLikeZip = (bytes) => bytes.length >= 4 && bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04
Try / catch
try {
await installSkill(zipUrl)
} catch (e) {
if (String(e).startsWith("unzip failed:")) {
if (String(e).includes("slip") || String(e).includes("path")) {
reportUntrustedArchive(zipUrl)
} else {
showHint("URL did not return a valid zip; check it in a browser")
}
}
} Prevention
- Only point at URLs that actually serve .zip content (verify in a browser first)
- Treat a zip-slip-wrapped failure as a sign of a malicious/malformed archive — change the source
- Retry once on corruption; persistent failures mean the source itself is bad
When it happens
Trigger: The downloaded bytes are not a valid zip (HTML error page saved as zip, truncated download, wrong Content-Type guess), or the archive contains entries with unsafe paths blocked by the zip-slip guard, or disk I/O errors during extraction.
Common situations: A direct-link source that is not actually a zip (server returned an HTML login/rate-limit page); a corrupted or partial download; an archive crafted with ../ entry names; temp disk full.
Related errors
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/1b2be984af192476.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/util/skill.go:742
// installFromZip 解压 zip 并安装其中的 skill
func installFromZip(data []byte) (*InstallSkillResult, error) {
tmpRoot := filepath.Join(TempDir, "ai", "skill-install", gulu.Rand.String(7))
if err := os.MkdirAll(tmpRoot, 0755); err != nil {
return nil, err
}
defer os.RemoveAll(tmpRoot)
zipPath := filepath.Join(tmpRoot, "src.zip")
if err := os.WriteFile(zipPath, data, 0644); err != nil {
return nil, err
}
unzipDir := filepath.Join(tmpRoot, "unzip")
if err := os.MkdirAll(unzipDir, 0755); err != nil {
return nil, err
}
// gulu.Zip.Unzip 已内置 zip-slip 路径穿越防护
if err := gulu.Zip.Unzip(zipPath, unzipDir); err != nil {
return nil, errors.New("unzip failed: " + err.Error())
}
skillDirs := findSkillDirs(unzipDir)
if len(skillDirs) == 0 {
return nil, errors.New("no SKILL.md found in the archive")
}
return installSkillDirs(skillDirs, unzipDir)
}
// findSkillDirs 在解压根下查找含 SKILL.md 的 skill 目录,返回相对 root 的路径。
// 递归下钻以兼容任意包裹层(codeload 会把仓库内容包在 <repo-name>/ 下),
// 但一旦某个目录被认定为 skill(直接含 SKILL.md)就停止下钻,避免误入 skill 内部的
// references/scripts 等子目录。识别的结构:
// - SKILL.md 直接在 root(无包裹)
// - <wrap>/SKILL.md(单层或多层包裹的单 skill)
// - <wrap>/skills/<name>/SKILL.md(集合仓库,wrap 可有可无)
func findSkillDirs(root string) []string {
if gulu.File.IsExist(filepath.Join(root, "SKILL.md")) {View on GitHub (pinned to 9f775e8a12)