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

  1. Replace the symlinked skill directory with a real copy of the package contents (e.g. `cp -rL`) in the external skills root.
  2. Remove any symlink or `..` components from the package path so the audit no longer flags it unsafe.
  3. Re-run the skill audit and confirm `path_unsafe` is false, then retry the import.
  4. 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

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


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)