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

  1. Extract archives and point at the extracted directory containing SKILL.md.
  2. Rename the prompt file to end in `.md` (any case).
  3. 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

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


AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18). Data as JSON: /api/errors/4c99507d8e625767. Report an issue: GitHub.