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
- Re-run the skills audit/scan so the index rebuilds relative_dir against the current flat layout.
- Ensure each skill package is a single directory directly under the skills root.
- Remove stale audit entries for skills that no longer exist on disk.
- 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
- Keep every skill package as one directory directly under the skills root — no nesting.
- Re-run the skills audit after moving, renaming, or reorganizing skill directories.
- Use only ASCII/UTF-8 directory names for skill packages.
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
- repo root does not exist: {}
- Runtime store root cannot contain '..' components
- only CodeWhale managed skills can be trusted
- installed package failed content digest validation: {err}
- skill version marker should have a parent directory
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/30a80e2f851a2291.
Report an issue: GitHub.