ultraworkers/claw-code · error · std::io::Error
skill source '{}' must be a directory with SKILL.md or a mar
Error message
skill source '{}' must be a directory with SKILL.md or a markdown file What it means
`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`.
Source
Thrown at rust/crates/commands/src/lib.rs:3893
"skill directory '{}' must contain SKILL.md",
source.display()
),
));
}
return Ok(SkillInstallSource::Directory {
root: source,
prompt_path,
});
}
if source
.extension()
.is_some_and(|ext| ext.to_string_lossy().eq_ignore_ascii_case("md"))
{
return Ok(SkillInstallSource::MarkdownFile { path: source });
}
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"skill source '{}' must be a directory with SKILL.md or a markdown file",
source.display()
),
))
}
fn derive_skill_install_name(
source: &SkillInstallSource,
declared_name: Option<&str>,
) -> std::io::Result<String> {
for candidate in [declared_name, source.fallback_name().as_deref()] {
if let Some(candidate) = candidate.and_then(sanitize_skill_invocation_name) {
return Ok(candidate);
}
}
View on GitHub (pinned to 08106b0c37)
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.
Example fix
# before /skills install ./my-skill.zip # skill source '...' must be a directory with SKILL.md or a markdown file # after unzip my-skill.zip -d my-skill && /skills install ./my-skill/my-skill
Defensive patterns
Strategy: validation
Validate before calling
use std::path::Path;
fn has_md_extension(p: &Path) -> bool {
p.extension().is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("md"))
}
// require source.is_dir() || has_md_extension(source) before install Try / catch
if let Err(e) = install_skill(src, cwd) {
if e.kind() == std::io::ErrorKind::InvalidInput
&& e.to_string().contains("must be a directory") { /* extract archive / rename to .md */ }
} Prevention
- Extract archives before installing
- Keep prompt files with an .md extension (any case)
- Reject .zip/.txt sources early in your wrapper scripts
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- unable to derive an installable invocation name from '{}'
- unable to resolve a skills install root; set CLAW_CONFIG_HOM
- skill source '{}' not found: {e}
- skill directory '{}' must contain SKILL.md
- old_string and new_string must differ
AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18).
Data as JSON: /api/errors/4c99507d8e625767.
Report an issue: GitHub.