{"record":{"id":"9a5545928cb5d265","repo":"Hmbown/CodeWhale","slug":"refusing-to-remove-skill-outside-codewhale-owned-roots","errorCode":null,"errorMessage":"refusing to remove skill outside Codewhale-owned roots","messagePattern":"refusing to remove skill outside Codewhale-owned roots","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/skills/mutation.rs","lineNumber":942,"sourceCode":"            action: SkillActionKind::Update,\n            name: skill_id.canonical_name,\n            scope,\n            safe_target_path: safe_display_path(&path, Some(ctx.workspace), ctx.home),\n            before_digest: before,\n            after_digest: None,\n            outcome: SkillMutationOutcome::NetworkDenied(host),\n        }),\n    }\n}\n\nfn remove_skill(\n    skill_id: AuditedSkillId,\n    expected_digest: Option<String>,\n    ctx: &MutationContext<'_>,\n) -> Result<SkillMutationReceipt> {\n    let (skill, path) = find_audited_skill(ctx, &skill_id)?;\n    if !skill.root.is_writable_owned() {\n        bail!(\"refusing to remove skill outside Codewhale-owned roots\");\n    }\n    if skill.source_kind != SkillSourceKind::CodeWhaleManaged {\n        bail!(\"only Codewhale managed skills can be removed\");\n    }\n    let skills_dir = validate_owned_skill_path(ctx, &skill, &path)?;\n    let before = verify_expected_digest(&path, expected_digest.as_deref())?;\n    let scope = match skill.root.kind {\n        SkillRootKind::CodeWhaleProject => SkillScope::Project,\n        SkillRootKind::CodeWhaleGlobal => SkillScope::Global,\n        _ => SkillScope::Logical,\n    };\n    let package_name = on_disk_package_name(&skill_id)?;\n    validate_owned_skill_path(ctx, &skill, &path)?;\n    install::uninstall(package_name, &skills_dir)?;\n    Ok(SkillMutationReceipt {\n        action: SkillActionKind::Remove,\n        name: skill_id.canonical_name,\n        scope,","sourceCodeStart":924,"sourceCodeEnd":960,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/skills/mutation.rs#L924-L960","documentation":"`remove_skill` only deletes skills inside roots marked writable/Codewhale-owned. Skills resolved from bundled, external, or otherwise non-owned roots cannot be removed through this API, protecting content Codewhale does not manage from deletion.","triggerScenarios":"Calling the `Remove` mutation (`execute_sync` → `remove_skill`) for a skill whose `root.is_writable_owned()` is false — bundled skills, unimported external skills, or owned dirs on read-only mounts.","commonSituations":"Trying to delete a bundled example skill; attempting to remove an external skill that was never imported; the configured skills directory is read-only (container image, restored snapshot).","solutions":["Only remove skills that live under a writable Codewhale-owned skills directory.","For external skills, remove the owned imported copy; the external original is left in place by design.","Remount/repair the skills directory so it is writable, and confirm the configured skills dir points at the real owned root."],"exampleFix":"// before\nmutation.execute_sync(Mutation::Remove { skill_id: external_skill_id, .. })?; // rejected\n// after\nmutation.execute_sync(Mutation::Remove { skill_id: owned_imported_id, .. })?; // owned copy only","handlingStrategy":"validation","validationCode":"let (skill, _) = find_audited_skill(ctx, &skill_id)?;\nif !skill.root.is_writable_owned() {\n    return Err(anyhow!(\"refusing to remove: skill is outside Codewhale-owned roots\"));\n}","typeGuard":"fn removable(skill: &AuditedSkill) -> bool {\n    skill.root.is_writable_owned() && skill.source_kind == SkillSourceKind::CodeWhaleManaged\n}","tryCatchPattern":null,"preventionTips":["Remove only managed skills inside owned roots.","Keep the skills directory on a writable filesystem.","Use remove for owned copies only; external originals stay in place."],"tags":["skills","permissions","remove"],"backgroundTag":"permission-denied","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"}