Hmbown/CodeWhale · error
refusing to mutate through non-directory Codewhale skills…
Error message
refusing to mutate through non-directory Codewhale skills path component {} What it means
checked_real_directory also rejects components that exist but are not directories (e.g. a file named 'skills' where a directory is expected). CodeWhale only mutates through real directory components in the owned skills path chain.
Solutions
- Remove or rename the offending non-directory path
- Create the expected directory in its place (e.g. mkdir ~/.codewhale/skills)
- Re-run the install so CodeWhale can create the proper structure
Example fix
// before $ cat > ~/.codewhale/skills # accidental file // after rm ~/.codewhale/skills && mkdir ~/.codewhale/skills
Defensive patterns
Strategy: validation
Validate before calling
let meta = std::fs::symlink_metadata(&skills_dir)?;
assert!(meta.is_dir(), "{} must be a directory", skills_dir.display()); Type guard
fn is_real_dir(p: &Path) -> bool {
std::fs::symlink_metadata(p).map(|m| m.is_dir()).unwrap_or(false)
} Try / catch
match install_remote(anchor, name) {
Err(e) if e.to_string().contains("non-directory") => eprintln!("a skills path component is a file; remove it and mkdir the directory"),
other => other?,
} Prevention
- Never create files where .codewhale/skills directories are expected
- Verify the path chain with `find ~/.codewhale -type d` before installs
- Recreate the owned directory structure after failed tooling runs
When it happens
Trigger: A path component in the anchor/.codewhale/skills chain exists as a regular file, FIFO, or other non-directory while a skill install/update/remove is attempted through that chain.
Common situations: A stray file accidentally created at ~/.codewhale/skills; a failed earlier setup leaving a partial file; a mount point replaced by a file.
Related errors
- audited owned root does not match mutation target
- invalid on-disk package directory for skill
- only CodeWhale managed skills can be trusted
- owned skill anchor does not exist
- owned skill parent does not exist
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/200873761d3245b8.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/skills/mutation.rs:154
match target {
SkillTargetScope::Project => Ok(workspace),
SkillTargetScope::Global => home.context("global skill mutations require a home directory"),
}
}
/// Return whether `path` is an existing real directory, rejecting links and
/// non-directory components. `symlink_metadata` is intentional: following a
/// link before checking it would turn a lexical CodeWhale-owned root into an
/// attacker-selected write target.
fn checked_real_directory(path: &Path) -> Result<bool> {
match fs::symlink_metadata(path) {
Ok(meta) if meta.file_type().is_symlink() => {
bail!(
"refusing to mutate symlinked Codewhale skills path component {}",
path.display()
)
}
Ok(meta) if !meta.is_dir() => bail!(
"refusing to mutate through non-directory Codewhale skills path component {}",
path.display()
),
Ok(_) => Ok(true),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(false),
Err(err) => Err(err).with_context(|| format!("failed to inspect {}", path.display())),
}
}
/// Validate the complete owned-root chain without following a symlink in the
/// workspace/home anchor, `.codewhale`, or `skills` component.
fn validate_owned_target_chain(
anchor: &Path,
skills_dir: &Path,
require_existing: bool,
) -> Result<()> {
let expected = anchor.join(".codewhale").join("skills");
if skills_dir != expected {View on GitHub (pinned to 73e0f67d83)