BigPizzaV3/CodexPlusPlus · error · anyhow::Error

下没有 SKILL.md,不是一个有效的 skill

Error message

{repo_path} 下没有 SKILL.md,不是一个有效的 skill

What it means

After extracting all entries, extract_skill_subtree verifies at least one written file was SKILL.md (wrote_manifest flag); otherwise it bails. A subtree without a manifest is not a valid skill and installing it would produce a broken link in the SSOT directory.

Solutions

  1. 核对 subdir 是否正确指向包含 SKILL.md 的目录,在 GitHub 上确认该路径下确实有 SKILL.md
  2. 若仓库结构更新,更新 SkillRepo 的 subdir 后重试
  3. 确认仓库遵循 skill 规范(SKILL.md 位于 skill 根);不规范仓库无法安装

Example fix

// before
// subdir = "skills/old-name" 但仓库已改名为 "skills/new-name"
extract_skill_subtree(&zip, "skills/old-name", &dest)?;
// after
extract_skill_subtree(&zip, "skills/new-name", &dest)?; // 含 SKILL.md 的目录
Defensive patterns

Strategy: try-catch

Validate before calling

// 安装前可先核对仓库结构:
// GET https://api.github.com/repos/{owner}/{name}/git/trees/{branch}?recursive=1
// 确认 subdir 下存在 SKILL.md

Try / catch

match extract_skill_subtree(&zip, repo_path, &dest) {
    Err(e) if e.to_string().contains("SKILL.md") => eprintln!("{} 不是有效的 skill 目录", repo_path),
    other => other?,
}

Prevention

When it happens

Trigger: zip 包中 repo_path 子树内不存在名为 SKILL.md 的文件——通常是 repo_path 填错了子目录,或该目录确实不是 skill 目录。

Common situations: SkillRepo.subdir 指向仓库中非 skill 目录(如文档目录);skill 结构不符合规范(清单用了别的文件名);仓库版本变更后目录结构变化导致旧 subdir 失效;zip 下载不完整。

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/34cfd09c165fc965. Report an issue: GitHub.

Appendix: source

Thrown at crates/codex-plus-core/src/skills.rs:828

                .with_context(|| format!("创建目录失败:{}", output_path.display()))?;
            continue;
        }
        if let Some(parent) = output_path.parent() {
            std::fs::create_dir_all(parent)
                .with_context(|| format!("创建目录失败:{}", parent.display()))?;
        }
        let mut contents = Vec::new();
        file.read_to_end(&mut contents)
            .with_context(|| format!("读取压缩包条目 {} 失败", file.name()))?;
        std::fs::write(&output_path, contents)
            .with_context(|| format!("写入 {} 失败", output_path.display()))?;
        if inner == SKILL_MANIFEST_FILE {
            wrote_manifest = true;
        }
    }

    if !wrote_manifest {
        anyhow::bail!("{repo_path} 下没有 SKILL.md,不是一个有效的 skill");
    }
    Ok(())
}

fn safe_relative_path(value: &str) -> anyhow::Result<PathBuf> {
    let mut relative = PathBuf::new();
    for component in Path::new(value).components() {
        match component {
            std::path::Component::Normal(part) => relative.push(part),
            std::path::Component::CurDir => {}
            _ => anyhow::bail!("压缩包条目越界:{value}"),
        }
    }
    if relative.as_os_str().is_empty() {
        anyhow::bail!("压缩包条目路径为空");
    }
    Ok(relative)
}

View on GitHub (pinned to b1ed92e5e4)