ultraworkers/claw-code · error · std::io::Error

skill directory '{}' must contain SKILL.md

Error message

skill directory '{}' must contain SKILL.md

What it means

In `resolve_skill_install_source` (commands/src/lib.rs:3872): the canonicalized source is a directory, but `<dir>/SKILL.md` is not a regular file. A skill directory is only installable when its SKILL.md prompt sits at the directory root. Returned as `ErrorKind::InvalidInput`.

Source

Thrown at rust/crates/commands/src/lib.rs:3872

fn resolve_skill_install_source(source: &str, cwd: &Path) -> std::io::Result<SkillInstallSource> {
    let candidate = PathBuf::from(source);
    let source = if candidate.is_absolute() {
        candidate
    } else {
        cwd.join(candidate)
    };
    let source = fs::canonicalize(&source).map_err(|e| {
        std::io::Error::new(
            e.kind(),
            format!("skill source '{}' not found: {e}", source.display()),
        )
    })?;

    if source.is_dir() {
        let prompt_path = source.join("SKILL.md");
        if !prompt_path.is_file() {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                format!(
                    "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 });

View on GitHub (pinned to 08106b0c37)

Solutions

  1. Point the install at the subdirectory that directly contains SKILL.md.
  2. If the prompt file is misnamed, rename it to exactly `SKILL.md`.
  3. For single-file skills, point at the markdown file itself instead of a directory.

Example fix

# before
/skills install ./claw-skills           # skill directory '...' must contain SKILL.md

# after
/skills install ./claw-skills/src/my-skill   # dir containing SKILL.md
Defensive patterns

Strategy: validation

Validate before calling

fn is_installable_skill_dir(p: &Path) -> bool {
    p.is_dir() && p.join("SKILL.md").is_file()   // exact case: SKILL.md
}

Type guard

fn classify_skill_source(p: &Path) -> Option<&'static str> {
    if p.is_dir() {
        if p.join("SKILL.md").is_file() { Some("directory") } else { None }
    } else if p.extension().is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("md")) {
        Some("markdown")
    } else { None }
}

Try / catch

match resolve_skill_install_source(src, cwd) {
    Err(e) if e.kind() == std::io::ErrorKind::InvalidInput
        && e.to_string().contains("must contain SKILL.md") => { /* descend into subdirs to find SKILL.md */ }
    other => other,
}

Prevention

When it happens

Trigger: Pointing `/skills install` at the root of a skill COLLECTION repo where each skill is a subdirectory; a skill dir whose prompt file is named `skill.md` (case matters), `README.md`, or `SKILL.MD`; SKILL.md being a directory or symlink to a non-file.

Common situations: Cloning a marketplace repo and installing the repo root instead of `repo/skills/my-skill`; renaming the prompt file during a migration from another convention; case-variant filenames after copying from a case-insensitive filesystem.

Related errors


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