{"record":{"id":"afbdd735de4ca3bb","repo":"Hmbown/CodeWhale","slug":"refusing-to-mutate-non-canonical-codewhale-skills-root","errorCode":null,"errorMessage":"refusing to mutate non-canonical Codewhale skills root {}","messagePattern":"refusing to mutate non-canonical Codewhale skills root (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/skills/mutation.rs","lineNumber":173,"sourceCode":"            \"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 {\n        bail!(\n            \"refusing to mutate non-canonical Codewhale skills root {}\",\n            skills_dir.display()\n        );\n    }\n\n    let anchor_exists = checked_real_directory(anchor)?;\n    if !anchor_exists {\n        if require_existing {\n            bail!(\"owned skill anchor {} does not exist\", anchor.display());\n        }\n        return Ok(());\n    }\n\n    let codewhale_dir = anchor.join(\".codewhale\");\n    let codewhale_exists = checked_real_directory(&codewhale_dir)?;\n    if !codewhale_exists {\n        if require_existing {\n            bail!(","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/skills/mutation.rs#L155-L191","documentation":"validate_owned_target_chain requires the skills directory to be exactly <anchor>/.codewhale/skills. Any other path — even a valid-looking directory — is refused, because mutation is only permitted on the canonical CodeWhale-owned skills root derived from the anchor.","triggerScenarios":"Calling install_remote, import_external, resolve_owned_target, prepare_owned_target, or validate_owned_root_descriptor with a skills_dir that differs lexically from anchor.join(\".codewhale\").join(\"skills\"), e.g. a custom or doubled path like anchor/.codewhale/skills/skills.","commonSituations":"Hand-edited configuration pointing at a custom skills folder; passing a nested skill directory instead of the skills root; using a relative path where the code compares against a constructed absolute path.","solutions":["Pass the canonical skills root: anchor/.codewhale/skills","Fix configuration so the skills dir is derived from the anchor rather than set independently","Ensure no extra segments were appended to the skills root path"],"exampleFix":"// before\nlet skills_dir = home.join(\".codewhale/skills/custom\");\n// after\nlet skills_dir = home.join(\".codewhale\").join(\"skills\");","handlingStrategy":"validation","validationCode":"let expected = anchor.join(\".codewhale\").join(\"skills\");\nassert_eq!(skills_dir, expected, \"skills_dir must be the canonical <anchor>/.codewhale/skills\");","typeGuard":"fn is_canonical_skills_root(anchor: &Path, skills_dir: &Path) -> bool {\n    skills_dir == anchor.join(\".codewhale\").join(\"skills\")\n}","tryCatchPattern":"match install_remote(anchor, name) {\n    Err(e) if e.to_string().contains(\"non-canonical\") => eprintln!(\"point skills_dir at <anchor>/.codewhale/skills\"),\n    other => other?,\n}","preventionTips":["Always derive the skills dir from the anchor, never configure it independently","Do not append extra segments to the skills root","Use the library's own resolver helpers instead of hand-built paths"],"tags":["skills","configuration","path-validation"],"backgroundTag":"invalid-config-value","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"}