{"record":{"id":"306a718996310497","repo":"Hmbown/CodeWhale","slug":"refusing-to-mutate-symlinked-codewhale-skills-path-component","errorCode":null,"errorMessage":"refusing to mutate symlinked Codewhale skills path component {}","messagePattern":"refusing to mutate symlinked Codewhale skills path component (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/skills/mutation.rs","lineNumber":149,"sourceCode":"fn owned_anchor<'a>(\n    workspace: &'a Path,\n    home: Option<&'a Path>,\n    target: SkillTargetScope,\n) -> Result<&'a Path> {\n    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,","sourceCodeStart":131,"sourceCodeEnd":167,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/skills/mutation.rs#L131-L167","documentation":"checked_real_directory walks skills path components using symlink_metadata (deliberately not following links) and refuses to mutate if any component is a symlink. Following the link first could redirect a CodeWhale-owned root to an attacker-chosen write target, so symlinked components are rejected outright.","triggerScenarios":"Installing, updating, or removing a skill where the anchor, .codewhale dir, skills dir, or any intermediate path component is a symlink (raised via validate_owned_target_chain, create_owned_directory, prepare_owned_target, or validate_owned_child).","commonSituations":"Users symlinking ~/.codewhale/skills to a dotfiles-managed or Dropbox-synced location; setups where the whole .codewhale directory is a symlink.","solutions":["Replace the symlink with a real directory (copy contents back in)","Symlink the individual skills inside the directory instead of any component of the owned chain","Point configuration at the real location and let CodeWhale create its own directory there"],"exampleFix":"// before\nln -s ~/dotfiles/skills ~/.codewhale/skills\n// after\nrm ~/.codewhale/skills\nmkdir ~/.codewhale/skills\ncp -r ~/dotfiles/skills/* ~/.codewhale/skills/","handlingStrategy":"validation","validationCode":"use std::fs::symlink_metadata;\nfor comp in [anchor.join(\".codewhale\"), anchor.join(\".codewhale\").join(\"skills\")] {\n    if let Ok(meta) = symlink_metadata(&comp) {\n        assert!(!meta.file_type().is_symlink(), \"{} is a symlink\", comp.display());\n    }\n}","typeGuard":"fn is_real_dir(p: &Path) -> bool {\n    std::fs::symlink_metadata(p).map(|m| !m.file_type().is_symlink() && m.is_dir()).unwrap_or(false)\n}","tryCatchPattern":"match install_remote(anchor, name) {\n    Err(e) if e.to_string().contains(\"symlinked\") => eprintln!(\"replace the symlink in the skills path with a real directory\"),\n    other => other?,\n}","preventionTips":["Do not symlink ~/.codewhale or its children to dotfiles/cloud-sync locations","Sync skill directories by copying or bind-mounting real directories","Check setups with `ls -la ~/.codewhale` for symlinks before installing"],"tags":["security","symlink","skills","path-safety"],"backgroundTag":"symlink-component-rejected","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"}