{"record":{"id":"1854ef27b9b538f2","repo":"Hmbown/CodeWhale","slug":"refusing-to-mutate-path-outside-direct-owned-ro","errorCode":null,"errorMessage":"refusing to mutate path {} outside direct owned root {}","messagePattern":"refusing to mutate path (.+?) outside direct owned root (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/skills/mutation.rs","lineNumber":295,"sourceCode":"    let target = target_scope_for_root(root)?;\n    let expected = resolve_owned_target(ctx.workspace, ctx.home, target)?;\n    if root.path != expected {\n        bail!(\n            \"audited owned root {} does not match mutation target {}\",\n            root.path.display(),\n            expected.display()\n        );\n    }\n    let anchor = owned_anchor(ctx.workspace, ctx.home, target)?;\n    validate_owned_target_chain(anchor, &expected, true)?;\n    Ok(expected)\n}\n\n/// Validate a real direct child of an already validated owned skills root.\n/// Returns false only when a missing child is permitted.\nfn validate_owned_child(skills_dir: &Path, child: &Path, require_existing: bool) -> Result<bool> {\n    if child.parent() != Some(skills_dir) {\n        bail!(\n            \"refusing to mutate path {} outside direct owned root {}\",\n            child.display(),\n            skills_dir.display()\n        );\n    }\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        )","sourceCodeStart":277,"sourceCodeEnd":313,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/skills/mutation.rs#L277-L313","documentation":"validate_owned_child requires the path being mutated to be a direct child of the already-validated owned skills root (child.parent() == skills_dir). Nested paths or sibling paths are rejected so mutations can only touch a skill package directory one level under the root, blocking writes elsewhere in the tree.","triggerScenarios":"Calling validate_owned_skill_path, install_remote, import_external, or update_skill with a child path that is not an immediate child of the validated skills_dir — e.g. a nested subdirectory, a relative path resolving outside the root, or a constructed path with extra components.","commonSituations":"A skill package name containing a path separator; hand-built paths like <root>/<vendor>/<skill> passed to install/import; symlinked or misconfigured install target; a caller joining user-controlled skill names onto the root.","solutions":["Pass the exact direct child path skills_dir.join(package_name); strip any nested components from the skill/package name.","Sanitize or reject package names containing '/', '\\\\', or '..' before calling the mutation API.","Verify the skills_dir you pass is the actual validated root, not a subdirectory of it."],"exampleFix":"// before\nlet target = skills_dir.join(\"vendor\").join(name);\ninstall_remote(&ctx, &target)?;\n// after\nlet name = sanitize_package_name(name)?; // rejects separators\nlet target = skills_dir.join(name);\ninstall_remote(&ctx, &target)?;","handlingStrategy":"validation","validationCode":"fn is_direct_child(skills_dir: &std::path::Path, child: &std::path::Path) -> bool {\n    child.parent() == Some(skills_dir)\n        && child.file_name().map(|n| !n.to_string_lossy().contains('/')).unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always construct child paths as skills_dir.join(single_component_name).","Sanitize package/skill names: reject '/', '\\\\', '..', and empty components.","Never accept user-supplied full paths for mutation APIs; accept names only."],"tags":["skills","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"}