Hmbown/CodeWhale · error
refusing to import unsafe external skill package
Error message
refusing to import unsafe external skill package
What it means
`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.
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.
Example fix
// before ln -s ~/dotfiles/skills/my-skill ~/.codewhale/skills-external/my-skill // after cp -rL ~/dotfiles/skills/my-skill ~/.codewhale/skills-external/my-skill
Defensive patterns
Strategy: validation
Validate before calling
if source_skill.path_unsafe {
return Err(anyhow!("source package path is unsafe (symlink/traversal); fix before import"));
} Type guard
fn safe_path(skill: &AuditedSkill) -> Option<&std::path::Path> {
(!skill.path_unsafe).then_some(skill.root.path.as_path())
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
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
- audited skill path does not match owned package
- refusing to copy symlink
- refusing to mutate symlinked Codewhale skills path component
- audited skill root identity changed; refusing mutation
- built-in plugin path may not be a symbolic link or reparse…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/2f2a6b5170a7580a.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/skills/mutation.rs:654
after_digest: None,
outcome: SkillMutationOutcome::NetworkDenied(host),
}),
}
}
fn import_external(
source_id: AuditedSkillId,
expected_digest: String,
target: SkillTargetScope,
conflict_policy: ConflictPolicy,
ctx: &MutationContext<'_>,
) -> Result<SkillMutationReceipt> {
let (source_skill, source_path) = find_audited_skill(ctx, &source_id)?;
if source_skill.source_kind != SkillSourceKind::CompatibleExternal {
bail!("import source must be a compatible external skill");
}
if source_skill.path_unsafe {
bail!("refusing to import unsafe external skill package");
}
let owned_snap = scan_with_configured(
ctx.workspace,
ctx.home,
ctx.configured_skills_dir,
SkillAuditMode::OwnedOnly,
None,
);
let owned_has_name = owned_snap
.skills
.iter()
.any(|s| s.id.canonical_name == source_id.canonical_name);
if !owned_has_name && !source_skill.import_candidate {
bail!("skill is not an import candidate");
}
View on GitHub (pinned to 73e0f67d83)