{"record":{"id":"2f2a6b5170a7580a","repo":"Hmbown/CodeWhale","slug":"refusing-to-import-unsafe-external-skill-package","errorCode":null,"errorMessage":"refusing to import unsafe external skill package","messagePattern":"refusing to import unsafe external skill package","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/skills/mutation.rs","lineNumber":654,"sourceCode":"            after_digest: None,\n            outcome: SkillMutationOutcome::NetworkDenied(host),\n        }),\n    }\n}\n\nfn import_external(\n    source_id: AuditedSkillId,\n    expected_digest: String,\n    target: SkillTargetScope,\n    conflict_policy: ConflictPolicy,\n    ctx: &MutationContext<'_>,\n) -> Result<SkillMutationReceipt> {\n    let (source_skill, source_path) = find_audited_skill(ctx, &source_id)?;\n    if source_skill.source_kind != SkillSourceKind::CompatibleExternal {\n        bail!(\"import source must be a compatible external skill\");\n    }\n    if source_skill.path_unsafe {\n        bail!(\"refusing to import unsafe external skill package\");\n    }\n\n    let owned_snap = scan_with_configured(\n        ctx.workspace,\n        ctx.home,\n        ctx.configured_skills_dir,\n        SkillAuditMode::OwnedOnly,\n        None,\n    );\n    let owned_has_name = owned_snap\n        .skills\n        .iter()\n        .any(|s| s.id.canonical_name == source_id.canonical_name);\n\n    if !owned_has_name && !source_skill.import_candidate {\n        bail!(\"skill is not an import candidate\");\n    }\n","sourceCodeStart":636,"sourceCodeEnd":672,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/skills/mutation.rs#L636-L672","documentation":"`import_external` refuses to copy an external skill package whose audit flagged `path_unsafe == true` — typically a symlink component, path traversal, or a location that resolves outside the expected skills root. This is a trust-boundary guard so an external package cannot smuggle files outside its directory during the copy.","triggerScenarios":"Calling `import_external` with a source skill whose audit produced `path_unsafe == true`, e.g. the package directory is a symlink, contains traversing relative paths, or resolves outside the scanned external root.","commonSituations":"A user hand-symlinked a skill folder (dotfiles repo, home dir) into the external skills directory; a downloaded skill archive extracted with symlinks preserved; a misconfigured external skills dir pointing at a non-canonical location.","solutions":["Replace the symlinked skill directory with a real copy of the package contents (e.g. `cp -rL`) in the external skills root.","Remove any symlink or `..` components from the package path so the audit no longer flags it unsafe.","Re-run the skill audit and confirm `path_unsafe` is false, then retry the import.","If the package came from an archive, re-extract with symlink preservation disabled."],"exampleFix":"// before\nln -s ~/dotfiles/skills/my-skill ~/.codewhale/skills-external/my-skill\n// after\ncp -rL ~/dotfiles/skills/my-skill ~/.codewhale/skills-external/my-skill","handlingStrategy":"validation","validationCode":"if source_skill.path_unsafe {\n    return Err(anyhow!(\"source package path is unsafe (symlink/traversal); fix before import\"));\n}","typeGuard":"fn safe_path(skill: &AuditedSkill) -> Option<&std::path::Path> {\n    (!skill.path_unsafe).then_some(skill.root.path.as_path())\n}","tryCatchPattern":null,"preventionTips":["Avoid symlinking skill folders into the skills directories.","Extract downloaded skill archives without preserving symlinks.","Keep external skill packages fully inside the scanned skills root."],"tags":["skills","security","symlink"],"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"}