{"record":{"id":"6c30a8fd70a5bfaf","repo":"Hmbown/CodeWhale","slug":"refusing-to-mutate-symlinked-codewhale-skills-path","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/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/skills/mutation.rs#L131-L167","documentation":"checked_real_directory uses symlink_metadata so it can see a symlink before following it; this bail fires when a component of the CodeWhale-owned skills path (anchor, .codewhale, or skills, or the target itself) is a symlink. Mutating through a symlink would redirect writes to an attacker- or user-selected location outside the owned root, so the mutation is refused. The symlinked path component is the input at fault; this is a generic guard shared by all owned-root mutation helpers.","triggerScenarios":"Thrown at crates/tui/src/skills/mutation.rs:149 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Remove the symlink and replace it with a real directory at that component","Re-create the skills directory (rm the link, then let CodeWhale create a real .codewhale/skills tree)","Do not point workspace/home paths for skills through links"],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}