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
- Point the install at the skill directory — the folder that contains SKILL.md
- 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
- Always pass the folder containing SKILL.md, never the file or an archive
- Extract downloaded archives before installing
- Validate user input paths with is_dir() plus a SKILL.md existence check
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
- Unable to determine installed skill directory after clone (n
- Source path does not exist: {source}
- Destination skill already exists: {}
- Skill content is empty
- Skill content is missing YAML front-matter (expected `---` d
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/ba9fdec7e3aa980a.
Report an issue: GitHub.