{"record":{"id":"ba9fdec7e3aa980a","repo":"zeroclaw-labs/zeroclaw","slug":"skill-source-must-be-a-directory-ba9fde","errorCode":null,"errorMessage":"Skill source must be a directory: {}","messagePattern":"Skill source must be a directory: (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/skills/mod.rs","lineNumber":2033,"sourceCode":"    let git_dir = skill_path.join(\".git\");\n    if git_dir.exists() {\n        std::fs::remove_dir_all(&git_dir)\n            .with_context(|| format!(\"failed to remove {}\", git_dir.display().to_string()))?;\n    }\n    Ok(())\n}\n\nfn copy_dir_recursive_secure(src: &Path, dest: &Path) -> Result<()> {\n    let src_meta = std::fs::symlink_metadata(src)\n        .with_context(|| format!(\"failed to read metadata for {}\", src.display().to_string()))?;\n    if src_meta.file_type().is_symlink() {\n        anyhow::bail!(\n            \"Refusing to copy symlinked skill source path: {}\",\n            src.display()\n        );\n    }\n    if !src_meta.is_dir() {\n        anyhow::bail!(\n            \"Skill source must be a directory: {}\",\n            src.display().to_string()\n        );\n    }\n\n    std::fs::create_dir_all(dest).with_context(|| {\n        format!(\n            \"failed to create destination {}\",\n            dest.display().to_string()\n        )\n    })?;\n    for entry in std::fs::read_dir(src)? {\n        let entry = entry?;\n        let src_path = entry.path();\n        let dest_path = dest.join(entry.file_name());\n        let metadata = std::fs::symlink_metadata(&src_path).with_context(|| {\n            format!(\n                \"failed to read metadata for {}\",","sourceCodeStart":2015,"sourceCodeEnd":2051,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/skills/mod.rs#L2015-L2051","documentation":"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.","triggerScenarios":"Pointing the local installer at my-skill/SKILL.md instead of my-skill/; passing a downloaded .tar.gz/.zip archive path unextracted.","commonSituations":"Users selecting the manifest file rather than its folder; archives downloaded but never extracted; drag-and-drop paths that land on a file.","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"],"exampleFix":"# before\nzeroclaw skills install ~/downloads/my-skill/SKILL.md\n\n# after\nzeroclaw skills install ~/downloads/my-skill","handlingStrategy":"validation","validationCode":"let p = std::path::Path::new(source);\nif !p.is_dir() {\n    anyhow::bail!(\"{source} is not a skill directory (expected the folder containing SKILL.md)\");\n}\ninstall_local_skill_source(source, &skills_path, allow_scripts)?;","typeGuard":"fn is_skill_directory(p: &std::path::Path) -> bool {\n    p.is_dir() && p.join(\"SKILL.md\").is_file()\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["skills","install","filesystem","validation"],"backgroundTag":"path-must-be-directory","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}