{"record":{"id":"200873761d3245b8","repo":"Hmbown/CodeWhale","slug":"refusing-to-mutate-through-non-directory-codewhale-skills","errorCode":null,"errorMessage":"refusing to mutate through non-directory Codewhale skills path component {}","messagePattern":"refusing to mutate through non-directory Codewhale skills path component (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/skills/mutation.rs","lineNumber":154,"sourceCode":"    match target {\n        SkillTargetScope::Project => Ok(workspace),\n        SkillTargetScope::Global => home.context(\"global skill mutations require a home directory\"),\n    }\n}\n\n/// Return whether `path` is an existing real directory, rejecting links and\n/// non-directory components. `symlink_metadata` is intentional: following a\n/// link before checking it would turn a lexical CodeWhale-owned root into an\n/// attacker-selected write target.\nfn checked_real_directory(path: &Path) -> Result<bool> {\n    match fs::symlink_metadata(path) {\n        Ok(meta) if meta.file_type().is_symlink() => {\n            bail!(\n                \"refusing to mutate symlinked Codewhale skills path component {}\",\n                path.display()\n            )\n        }\n        Ok(meta) if !meta.is_dir() => bail!(\n            \"refusing to mutate through non-directory Codewhale skills path component {}\",\n            path.display()\n        ),\n        Ok(_) => Ok(true),\n        Err(err) if err.kind() == ErrorKind::NotFound => Ok(false),\n        Err(err) => Err(err).with_context(|| format!(\"failed to inspect {}\", path.display())),\n    }\n}\n\n/// Validate the complete owned-root chain without following a symlink in the\n/// workspace/home anchor, `.codewhale`, or `skills` component.\nfn validate_owned_target_chain(\n    anchor: &Path,\n    skills_dir: &Path,\n    require_existing: bool,\n) -> Result<()> {\n    let expected = anchor.join(\".codewhale\").join(\"skills\");\n    if skills_dir != expected {","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/skills/mutation.rs#L136-L172","documentation":"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.","triggerScenarios":"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.","commonSituations":"A stray file accidentally created at ~/.codewhale/skills; a failed earlier setup leaving a partial file; a mount point replaced by a file.","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"],"exampleFix":"// before\n$ cat > ~/.codewhale/skills  # accidental file\n// after\nrm ~/.codewhale/skills && mkdir ~/.codewhale/skills","handlingStrategy":"validation","validationCode":"let meta = std::fs::symlink_metadata(&skills_dir)?;\nassert!(meta.is_dir(), \"{} must be a directory\", skills_dir.display());","typeGuard":"fn is_real_dir(p: &Path) -> bool {\n    std::fs::symlink_metadata(p).map(|m| m.is_dir()).unwrap_or(false)\n}","tryCatchPattern":"match install_remote(anchor, name) {\n    Err(e) if e.to_string().contains(\"non-directory\") => eprintln!(\"a skills path component is a file; remove it and mkdir the directory\"),\n    other => other?,\n}","preventionTips":["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"],"tags":["skills","filesystem","path-safety"],"backgroundTag":"path-is-not-a-directory","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}