BigPizzaV3/CodexPlusPlus · error · anyhow::Error

skill 中不允许包含符号链接:

Error message

skill 中不允许包含符号链接:{}

What it means

While iterating zip entries, extract_skill_subtree checks file.is_symlink() and bails naming the entry. Symlink entries could point outside the destination on extraction — the same policy as codex's built-in skill-installer — so any archive containing one is refused entirely.

Solutions

  1. 改用不含 symlink 的 zip 包重新安装(联系仓库作者移除 symlink 或用真实文件替代)
  2. 若自有仓库,把 symlink 替换为复制出来的真实文件后重新打包
  3. 从可信来源下载 skill 包,避免安装未知第三方仓库的 skill
Defensive patterns

Strategy: try-catch

Try / catch

match state.install_from_zip(&zip_bytes, &repo_path) {
    Err(e) if e.to_string().contains("符号链接") => {
        eprintln!("该 skill 包含 symlink,已拒绝安装,请改用无 symlink 的包");
    }
    other => other?,
}

Prevention

When it happens

Trigger: 从 GitHub 仓库(或任意来源)下载的 zip 中包含 symlink 条目,extract_skill_subtree 或 install_from_zip 解压该包时触发。

Common situations: 仓库里作者为共享文件创建了 symlink(macOS/Linux 常见);构建产物打包时把链接原样打进 zip;恶意包注入 symlink 实施攻击。

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


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

Appendix: source

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

    zip_bytes: &[u8],
    repo_path: &str,
    destination: &Path,
) -> anyhow::Result<()> {
    let repo_path = repo_path.trim_matches('/');
    if repo_path.is_empty() {
        anyhow::bail!("skill 在仓库中的路径不能为空");
    }
    let prefix = format!("{repo_path}/");
    let mut archive =
        zip::ZipArchive::new(Cursor::new(zip_bytes)).context("skill 仓库压缩包无法解析")?;
    let mut wrote_manifest = false;

    for index in 0..archive.len() {
        let mut file = archive
            .by_index(index)
            .with_context(|| format!("读取压缩包条目 {index} 失败"))?;
        if file.is_symlink() {
            anyhow::bail!("skill 中不允许包含符号链接:{}", file.name());
        }
        let Some(relative) = crate::plugin_marketplace::zip_entry_relative_path(file.name()) else {
            continue;
        };
        // zip 内部的分隔符恒为 '/',但上面拿回来的是 PathBuf,在 Windows 上
        // to_str() 会渲染成 '\',跟用 '/' 拼出来的 prefix 永远匹配不上——结果就是
        // 一个文件都解不出来,报「没有 SKILL.md」。这里统一拼回 '/' 再比。
        let relative = relative
            .components()
            .filter_map(|component| component.as_os_str().to_str())
            .collect::<Vec<_>>()
            .join("/");
        let Some(inner) = relative.strip_prefix(prefix.as_str()) else {
            continue;
        };
        if inner.is_empty() {
            continue;
        }

View on GitHub (pinned to b1ed92e5e4)