{"record":{"id":"5aa75bb6d8f40cfc","repo":"Hmbown/CodeWhale","slug":"owned-skill-path-escapes-direct-root","errorCode":null,"errorMessage":"owned skill path {} escapes direct root {}","messagePattern":"owned skill path (.+?) escapes direct root (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/skills/mutation.rs","lineNumber":318,"sourceCode":"    }\n    let exists = checked_real_directory(child)?;\n    if !exists {\n        if require_existing {\n            bail!(\"owned skill path {} does not exist\", child.display());\n        }\n        return Ok(false);\n    }\n\n    let canonical_root = fs::canonicalize(skills_dir).with_context(|| {\n        format!(\n            \"failed to resolve owned skill root {}\",\n            skills_dir.display()\n        )\n    })?;\n    let canonical_child = fs::canonicalize(child)\n        .with_context(|| format!(\"failed to resolve owned skill path {}\", child.display()))?;\n    if canonical_child.parent() != Some(canonical_root.as_path()) {\n        bail!(\n            \"owned skill path {} escapes direct root {}\",\n            child.display(),\n            skills_dir.display()\n        );\n    }\n    Ok(true)\n}\n\nfn validate_owned_skill_path(\n    ctx: &MutationContext<'_>,\n    skill: &AuditedSkill,\n    path: &Path,\n) -> Result<PathBuf> {\n    if skill.id.root_id != skill.root.id {\n        bail!(\"audited skill root identity changed; refusing mutation\");\n    }\n    let skills_dir = validate_owned_root_descriptor(ctx, &skill.root)?;\n    let package_name = on_disk_package_name(&skill.id)?;","sourceCodeStart":300,"sourceCodeEnd":336,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/skills/mutation.rs#L300-L336","documentation":"After existence is confirmed, validate_owned_child canonicalizes both the root and the child and requires the canonicalized child to remain a direct child of the canonicalized root. This is the symlink/alias defense: if the child resolves (via symlinks) outside the owned root, the mutation is refused to prevent writing through a link into another location.","triggerScenarios":"Calling validate_owned_skill_path, install_remote, import_external, or update_skill where the child directory is (or contains) a symlink whose canonical target's parent is not the canonicalized skills root — e.g. the skill dir is a symlink to ~/elsewhere/skill, or the root itself is reached through a different symlinked path component.","commonSituations":"User symlinked a skill folder into the skills root to share it; the project skills directory is itself a symlink so canonicalization shifts the parent; dotfile managers (stow, chezmoi) replace skill dirs with links.","solutions":["Replace the symlink with a real directory (copy the content) in the owned skills root.","Ensure the canonicalized child resolves inside the canonicalized root — check with `readlink -f` on both paths before mutating.","If the whole skills root is symlinked, mutate using the canonical path so both sides resolve identically, or reconfigure the root to its real location."],"exampleFix":"// before\nln -s ~/shared/my-skill .codewhale/skills/my-skill  # mutation refused\n// after\nrm .codewhale/skills/my-skill\ncp -r ~/shared/my-skill .codewhale/skills/my-skill","handlingStrategy":"validation","validationCode":"fn resolves_inside_root(skills_dir: &std::path::Path, child: &std::path::Path) -> std::io::Result<bool> {\n    let root = std::fs::canonicalize(skills_dir)?;\n    let kid = std::fs::canonicalize(child)?;\n    Ok(kid.parent() == Some(root.as_path()))\n}","typeGuard":null,"tryCatchPattern":"// detect symlinked skill dirs up front\nif fs::symlink_metadata(child).map(|m| m.file_type().is_symlink()).unwrap_or(false) {\n    eprintln!(\"{} is a symlink; copy it into the root as a real directory\", child.display());\n}","preventionTips":["Do not symlink skill folders into the owned skills root; copy them instead.","Audit the skills root for symlinks (find . -type l) before bulk mutations.","Configure skills root paths to their canonical real locations so canonicalization does not shift the parent."],"tags":["skills","symlink","path-safety","path-traversal","rust"],"backgroundTag":"path-traversal-blocked","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"}