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

Skill source must be a directory: {}

Error message

Skill source must be a directory: {}

What it means

The source path exists and is not a symlink, but is_dir() is false — a regular file was passed to install_local_skill_source. Skills are directories that contain a SKILL.md; the installer copies a directory tree, not a single file.

Source

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

    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)? {
        let entry = entry?;
        let src_path = entry.path();
        let dest_path = dest.join(entry.file_name());
        let metadata = std::fs::symlink_metadata(&src_path).with_context(|| {
            format!(
                "failed to read metadata for {}",

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Point the install at the skill directory — the folder that contains SKILL.md
  2. If you downloaded an archive, extract it first and install the extracted directory

Example fix

# before
zeroclaw skills install ~/downloads/my-skill/SKILL.md

# after
zeroclaw skills install ~/downloads/my-skill
Defensive patterns

Strategy: validation

Validate before calling

let p = std::path::Path::new(source);
if !p.is_dir() {
    anyhow::bail!("{source} is not a skill directory (expected the folder containing SKILL.md)");
}
install_local_skill_source(source, &skills_path, allow_scripts)?;

Type guard

fn is_skill_directory(p: &std::path::Path) -> bool {
    p.is_dir() && p.join("SKILL.md").is_file()
}

Prevention

When it happens

Trigger: Pointing the local installer at my-skill/SKILL.md instead of my-skill/; passing a downloaded .tar.gz/.zip archive path unextracted.

Common situations: Users selecting the manifest file rather than its folder; archives downloaded but never extracted; drag-and-drop paths that land on a file.

Related errors


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