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

unable to derive an installable invocation name from '{}'

Error message

unable to derive an installable invocation name from '{}'

What it means

`derive_skill_install_name` (commands/src/lib.rs:3912) tries two candidates in order: the skill's frontmatter name, then the fallback (directory name, or file stem for a bare .md file). Each is run through `sanitize_skill_invocation_name`, which lowercases, keeps only ASCII alphanumerics and `-_.`, converts whitespace and `/`/`\` to `-`, and trims leading/trailing separators. If BOTH candidates sanitize to an empty string, this InvalidInput error is raised.

Source

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

        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);
        }
    }

    Err(std::io::Error::new(
        std::io::ErrorKind::InvalidInput,
        format!(
            "unable to derive an installable invocation name from '{}'",
            source.report_path().display()
        ),
    ))
}

fn sanitize_skill_invocation_name(candidate: &str) -> Option<String> {
    let trimmed = candidate
        .trim()
        .trim_start_matches('/')
        .trim_start_matches('$');
    if trimmed.is_empty() {
        return None;
    }

    let mut sanitized = String::new();

View on GitHub (pinned to 08106b0c37)

Solutions

  1. Add or fix a `name:` field in the SKILL.md frontmatter using at least one ASCII letter or digit.
  2. Rename the source directory (or .md file) so its stem contains ASCII alphanumerics, e.g. `debug-skill`.
  3. Pre-check the sanitized result yourself: lowercase, drop non-[a-z0-9-_.] chars, and confirm something survives.

Example fix

# before: dir '调试技能/SKILL.md' with no frontmatter name
/skills install ./调试技能   # unable to derive an installable invocation name from '...'

# after: SKILL.md frontmatter
---
name: debug-skill
---
/skills install ./调试技能   # installs as 'debug-skill'
Defensive patterns

Strategy: validation

Validate before calling

fn sanitize_preview(name: &str) -> String {
    name.trim().trim_start_matches(['/','$']).to_ascii_lowercase()
        .chars().map(|c| if c.is_ascii_alphanumeric() || matches!(c,'-'|'_'|'.') { c } else if c.is_whitespace() || matches!(c,'/'|'\\') { '-' } else { '\0' })
        .filter(|&c| c != '\0').collect()  // approximate; empty result => will fail
}
// require a non-empty result containing an ascii alphanumeric before install

Try / catch

if let Err(e) = install_skill(src, cwd) {
    if e.to_string().contains("unable to derive an installable invocation name") {
        // set frontmatter `name:` with ascii letters, or rename the dir/file
    }
}

Prevention

When it happens

Trigger: A skill directory named entirely in a non-Latin script (e.g. `调试技能` — every CJK char is dropped because only `is_ascii_alphanumeric` is kept); a name made solely of punctuation/emoji (`!!!`, `#1`); an empty frontmatter `name:` plus a punctuated directory name; a file stem like `---`.

Common situations: Skills authored by non-English teams whose directory name is the native-language skill name; markdown files named with decorative emoji prefixes; frontmatter missing a `name` field combined with a symbolic directory name.

Related errors


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