{"record":{"id":"4c99507d8e625767","repo":"ultraworkers/claw-code","slug":"skill-source-must-be-a-directory-with-skill-m","errorCode":null,"errorMessage":"skill source '{}' must be a directory with SKILL.md or a markdown file","messagePattern":"skill source '(.+?)' must be a directory with SKILL\\.md or a markdown file","errorType":"validation","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"rust/crates/commands/src/lib.rs","lineNumber":3893,"sourceCode":"                    \"skill directory '{}' must contain SKILL.md\",\n                    source.display()\n                ),\n            ));\n        }\n        return Ok(SkillInstallSource::Directory {\n            root: source,\n            prompt_path,\n        });\n    }\n\n    if source\n        .extension()\n        .is_some_and(|ext| ext.to_string_lossy().eq_ignore_ascii_case(\"md\"))\n    {\n        return Ok(SkillInstallSource::MarkdownFile { path: source });\n    }\n\n    Err(std::io::Error::new(\n        std::io::ErrorKind::InvalidInput,\n        format!(\n            \"skill source '{}' must be a directory with SKILL.md or a markdown file\",\n            source.display()\n        ),\n    ))\n}\n\nfn derive_skill_install_name(\n    source: &SkillInstallSource,\n    declared_name: Option<&str>,\n) -> std::io::Result<String> {\n    for candidate in [declared_name, source.fallback_name().as_deref()] {\n        if let Some(candidate) = candidate.and_then(sanitize_skill_invocation_name) {\n            return Ok(candidate);\n        }\n    }\n","sourceCodeStart":3875,"sourceCodeEnd":3911,"githubUrl":"https://github.com/ultraworkers/claw-code/blob/08106b0c3771ef5b4a5aa176acccd460e88b7325/rust/crates/commands/src/lib.rs#L3875-L3911","documentation":"`resolve_skill_install_source` (commands/src/lib.rs:3893): the source exists (it canonicalized successfully) but is neither a directory-with-SKILL.md nor a file whose extension is `md` (compared case-insensitively). The extension check runs only on non-directories, so symlinks-to-files without an .md extension also land here. `ErrorKind::InvalidInput`.","triggerScenarios":"Pointing `/skills install` at a `.zip`/`.tar.gz` archive of the skill; a `.txt`/`.json` prompt file; a FIFO/socket; a symlink whose target lacks the .md extension.","commonSituations":"Downloading a skill release as an archive and installing it without extracting; skills authored as `prompt.txt` by tools with a different convention; case variants like `.MD` work, but `.markdown` does not.","solutions":["Extract archives and point at the extracted directory containing SKILL.md.","Rename the prompt file to end in `.md` (any case).","If the source is a directory, ensure SKILL.md exists at its root so the directory branch is taken."],"exampleFix":"# before\n/skills install ./my-skill.zip    # skill source '...' must be a directory with SKILL.md or a markdown file\n\n# after\nunzip my-skill.zip -d my-skill && /skills install ./my-skill/my-skill","handlingStrategy":"validation","validationCode":"use std::path::Path;\n\nfn has_md_extension(p: &Path) -> bool {\n    p.extension().is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case(\"md\"))\n}\n// require source.is_dir() || has_md_extension(source) before install","typeGuard":null,"tryCatchPattern":"if let Err(e) = install_skill(src, cwd) {\n    if e.kind() == std::io::ErrorKind::InvalidInput\n        && e.to_string().contains(\"must be a directory\") { /* extract archive / rename to .md */ }\n}","preventionTips":["Extract archives before installing","Keep prompt files with an .md extension (any case)","Reject .zip/.txt sources early in your wrapper scripts"],"tags":["skills","install","file-format","invalid-input"],"backgroundTag":"unsupported-file-type","analyzedSha":"08106b0c3771ef5b4a5aa176acccd460e88b7325","analyzedAt":"2026-08-18T00:29:38.590Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}