Hmbown/CodeWhale · error

refusing to mutate path

Error message

refusing to mutate path {} outside direct owned root {}

What it means

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.

Solutions

  1. Pass the exact direct child path skills_dir.join(package_name); strip any nested components from the skill/package name.
  2. Sanitize or reject package names containing '/', '\\', or '..' before calling the mutation API.
  3. Verify the skills_dir you pass is the actual validated root, not a subdirectory of it.

Example fix

// before
let target = skills_dir.join("vendor").join(name);
install_remote(&ctx, &target)?;
// after
let name = sanitize_package_name(name)?; // rejects separators
let target = skills_dir.join(name);
install_remote(&ctx, &target)?;
Defensive patterns

Strategy: validation

Validate before calling

fn is_direct_child(skills_dir: &std::path::Path, child: &std::path::Path) -> bool {
    child.parent() == Some(skills_dir)
        && child.file_name().map(|n| !n.to_string_lossy().contains('/')).unwrap_or(false)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/1854ef27b9b538f2. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/skills/mutation.rs:295

    let target = target_scope_for_root(root)?;
    let expected = resolve_owned_target(ctx.workspace, ctx.home, target)?;
    if root.path != expected {
        bail!(
            "audited owned root {} does not match mutation target {}",
            root.path.display(),
            expected.display()
        );
    }
    let anchor = owned_anchor(ctx.workspace, ctx.home, target)?;
    validate_owned_target_chain(anchor, &expected, true)?;
    Ok(expected)
}

/// Validate a real direct child of an already validated owned skills root.
/// Returns false only when a missing child is permitted.
fn validate_owned_child(skills_dir: &Path, child: &Path, require_existing: bool) -> Result<bool> {
    if child.parent() != Some(skills_dir) {
        bail!(
            "refusing to mutate path {} outside direct owned root {}",
            child.display(),
            skills_dir.display()
        );
    }
    let exists = checked_real_directory(child)?;
    if !exists {
        if require_existing {
            bail!("owned skill path {} does not exist", child.display());
        }
        return Ok(false);
    }

    let canonical_root = fs::canonicalize(skills_dir).with_context(|| {
        format!(
            "failed to resolve owned skill root {}",
            skills_dir.display()
        )

View on GitHub (pinned to 73e0f67d83)