zeroclaw-labs/zeroclaw · error · anyhow::Error

Refusing to copy symlinked skill source path: {}

Error message

Refusing to copy symlinked skill source path: {}

What it means

copy_dir_recursive_secure stats the source with symlink_metadata (which does not follow links) and refuses to copy when the top-level source path itself is a symlink. The installer only copies a real directory the user actually named, never whatever a link resolves to.

Source

Thrown at crates/zeroclaw-runtime/src/skills/mod.rs:2027

    }

    anyhow::bail!("Skill security audit failed: {}", report.summary());
}

fn remove_git_metadata(skill_path: &Path) -> Result<()> {
    let git_dir = skill_path.join(".git");
    if git_dir.exists() {
        std::fs::remove_dir_all(&git_dir)
            .with_context(|| format!("failed to remove {}", git_dir.display().to_string()))?;
    }
    Ok(())
}

fn copy_dir_recursive_secure(src: &Path, dest: &Path) -> Result<()> {
    let src_meta = std::fs::symlink_metadata(src)
        .with_context(|| format!("failed to read metadata for {}", src.display().to_string()))?;
    if src_meta.file_type().is_symlink() {
        anyhow::bail!(
            "Refusing to copy symlinked skill source path: {}",
            src.display()
        );
    }
    if !src_meta.is_dir() {
        anyhow::bail!(
            "Skill source must be a directory: {}",
            src.display().to_string()
        );
    }

    std::fs::create_dir_all(dest).with_context(|| {
        format!(
            "failed to create destination {}",
            dest.display().to_string()
        )
    })?;
    for entry in std::fs::read_dir(src)? {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Resolve the link and pass the real directory: run 'realpath <path>' and install that path
  2. In code, std::fs::canonicalize the path before calling install_local_skill_source
  3. Restructure so the skill directory is a real directory in (or copied into) the workspace

Example fix

# before
zeroclaw skills install ~/links/my-skill   # ~/links/my-skill is a symlink

# after
zeroclaw skills install "$(realpath ~/links/my-skill)"
Defensive patterns

Strategy: validation

Validate before calling

// Resolve links before installing:
let real = std::fs::canonicalize(&source)
    .with_context(|| format!("resolve skill source {source}"))?;
install_local_skill_source(real.to_str().unwrap(), &skills_path, allow_scripts)?;

Type guard

fn is_real_directory(p: &std::path::Path) -> bool {
    std::fs::symlink_metadata(p)
        .map(|m| !m.file_type().is_symlink() && m.is_dir())
        .unwrap_or(false)
}

Prevention

When it happens

Trigger: skills install <path> where path is a symlink to the actual skill directory — common with dotfiles-style management, stow, or a monorepo checkout where the skills folder is linked into the workspace.

Common situations: Skills managed via symlink farms; workspaces assembled from linked checkouts; shortcuts created for convenience pointing at the real skill repo.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/30fe7f08cc95dcba. Report an issue: GitHub.