siyuan-note/siyuan · error

unzip failed:

Error message

unzip failed: 

What it means

installFromZip writes the downloaded bytes to a temp file and extracts it with gulu.Zip.Unzip (which includes zip-slip protection). Any extraction failure — corrupted archive, non-zip content, unsupported compression — is wrapped as "unzip failed: <underlying error>".

Source

Thrown at kernel/util/skill.go:736

// 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 8641553a1f)

Solutions

  1. Confirm the source URL returns an actual application/zip payload (curl -sI <url> and check Content-Type and size)
  2. Check the wrapped underlying error: 'zip: not a valid zip file' means wrong content; 'unexpected EOF' means truncation — re-download
  3. Recreate the archive with a standard zip tool without encryption or exotic compression
  4. Test unzipping the file locally before installing
Defensive patterns

Strategy: validation

Validate before calling

const buf = await fetch(url).then(r => r.arrayBuffer()); if (new Uint8Array(buf.slice(0,2)).join(",") !== "80,75") throw new Error("not a zip file (missing PK header)");

Type guard

function looksLikeZip(bytes) { return bytes && bytes.length > 4 && bytes[0] === 0x50 && bytes[1] === 0x4b; }

Try / catch

try { await installSkill(src); } catch (e) { if (/unzip failed: zip: not a valid zip/.test(e.message)) { verifyUrlServesZip(); } else throw e; }

Prevention

When it happens

Trigger: The bytes passed to installFromZip are not a valid zip: the URL returned an HTML error/login page with 200, the download was truncated, the file is gzip/tar instead of zip, or the archive uses an unsupported compression method or is password-protected.

Common situations: Pastebin/proxy URLs that serve HTML with status 200; manually renamed .tar.gz files; corrupted downloads over flaky networks; archives produced with non-standard zip tooling; content behind a redirect that yields a login page.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/ed5c53efba509a2e. Report an issue: GitHub.