Hmbown/CodeWhale · error · anyhow::Error

invalid on-disk package directory for skill '{}'

Error message

invalid on-disk package directory for skill '{}'

What it means

on_disk_package_name derives the on-disk package directory from an audited skill's relative_dir: it must consist of exactly one normal path component (the folder sits directly under the skills root, e.g. 'my-skill'). The error fires when relative_dir is empty, '.', '..', the root itself, has multiple components ('a/b'), or is not valid UTF-8. The message reports skill_id.canonical_name, which may differ from the directory name because the audit index stores a normalized lookup key while installs use raw frontmatter names.

Source

Thrown at crates/tui/src/skills/mutation.rs:547

        .into_iter()
        .find(|s| &s.id == skill_id)
        .with_context(|| format!("audited skill {} not found", skill_id.canonical_name))?;
    let path = skill.root.path.join(&skill.id.relative_dir);
    Ok((skill, path))
}

/// Directory segment under the skills root. Prefer this over `canonical_name`
/// when calling install helpers that join `skills_dir / name` — installs use
/// raw frontmatter names, while audit stores a normalized lookup key.
fn on_disk_package_name(skill_id: &AuditedSkillId) -> Result<&str> {
    let mut components = skill_id.relative_dir.components();
    let name = match (components.next(), components.next()) {
        (Some(Component::Normal(name)), None) => name.to_str(),
        _ => None,
    }
    .filter(|name| !name.is_empty())
    .ok_or_else(|| {
        anyhow::anyhow!(
            "invalid on-disk package directory for skill '{}'",
            skill_id.canonical_name
        )
    })?;
    Ok(name)
}

fn verify_expected_digest(path: &Path, expected: Option<&str>) -> Result<Option<String>> {
    let current = package_digest::compute_package_digest(path)
        .with_context(|| format!("cannot digest {}", path.display()))?;
    if let Some(expected) = expected
        && expected != current
    {
        bail!(
            "skill content changed since audit (expected {expected}, found {current}); \
             re-review before mutating"
        );
    }

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Re-run the skills audit/scan so the index rebuilds relative_dir against the current flat layout.
  2. Ensure each skill package is a single directory directly under the skills root.
  3. Remove stale audit entries for skills that no longer exist on disk.
  4. If the directory name contains non-UTF-8 bytes, rename it to a UTF-8 name and re-audit.
Defensive patterns

Strategy: validation

Validate before calling

use std::path::{Component, Path};

fn is_flat_skill_dir(relative_dir: &Path) -> bool {
    let mut components = relative_dir.components();
    matches!((components.next(), components.next()),
        (Some(Component::Normal(_)), None))
}

if !is_flat_skill_dir(&skill_id.relative_dir) {
    anyhow::bail!("skill '{}' is not a flat directory under the skills root", skill_id.canonical_name);
}

Prevention

When it happens

Trigger: Calling an install/mutation helper with an AuditedSkillId whose relative_dir is not a single flat segment: audit entries produced by an older nested layout ('pack/skill'), a manually reorganized skills directory, or a stale audit cache whose relative paths no longer match the flat install layout.

Common situations: Skills directory reorganized by hand (nested folders) after the audit ran; audit index from an older version; a skill dir renamed or moved so the cached relative_dir resolves to root.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/30a80e2f851a2291. Report an issue: GitHub.