Hmbown/CodeWhale · error

[plugin].name is not a safe directory name

Error message

[plugin].name is not a safe directory name: {error:#}

What it means

The manifest validated structurally, but its `[plugin].name` fails `validate_skill_name_segment`: the name is not usable as a single safe directory segment (contains path separators, leading dots like `..`, empty, or forbidden characters). This is a fail-closed guard so a hostile manifest cannot write outside the plugins directory.

Solutions

  1. Change the manifest's [plugin].name to a simple identifier: letters, digits, hyphens, underscores only.
  2. Remove path separators or '..' from the name; use the bare plugin slug.
  3. If the name comes from a template generator, fix the generator's slugification step.
  4. Reinstall after fixing the manifest; the staged name is used as the install directory.

Example fix

// before: plugin.json
{ "plugin": { "name": "../../.ssh", "version": "1.0.0" } }
// after
{ "plugin": { "name": "my-plugin", "version": "1.0.0" } }
Defensive patterns

Strategy: validation

Validate before calling

const name = manifest.plugin.name;
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) {
  throw new Error(`[plugin].name not a safe directory segment: ${name}`);
}

Type guard

const isSafePluginName = (n) => typeof n === 'string' && n.length > 0 &&
  !n.includes('/') && !n.includes('\\') && n !== '.' && n !== '..' &&
  /^[\w-]+$/.test(n);

Try / catch

try {
  await codewhale.plugins.install(srcDir);
} catch (e) {
  if (e.message.includes('[plugin].name is not a safe directory name')) {
    console.error('fix the name field in the plugin manifest');
  } else throw e;
}

Prevention

When it happens

Trigger: `validate_staged` (called from `stage_local_copy` and `stage_tarball`) runs `validate_skill_name_segment(&name)` on the manifest-declared plugin name during install staging.

Common situations: A malicious or typo'd manifest with name "../../evil", scoped names like "@scope/pkg", names with spaces/slashes, or an empty name field.

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/6671d8783f47a526. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/plugins/install/stage.rs:79

    fs::create_dir_all(user_plugins_dir).with_context(|| {
        format!(
            "failed to create user plugins directory {}",
            user_plugins_dir.display()
        )
    })
}

/// Validate the staged tree and return the manifest name + content hash.
pub(super) fn validate_staged(staged_path: &Path) -> Result<(String, String)> {
    let manifest_path = crate::plugins::agent_plugin::resolve_manifest_path(staged_path)
        .ok_or_else(|| {
            anyhow::anyhow!("staged bundle has no plugin.json, .claude-plugin/plugin.json, kimi.plugin.json, or plugin.toml")
        })?;
    let validated = PluginManifest::validate_from_path(&manifest_path)
        .map_err(|error| anyhow::anyhow!("staged plugin manifest failed validation: {error}"))?;
    let name = validated.manifest.plugin.name.clone();
    validate_skill_name_segment(&name).map_err(|error| {
        anyhow::anyhow!("[plugin].name is not a safe directory name: {error:#}")
    })?;
    Ok((name, validated.content_hash))
}

/// Copy a local bundle directory into staging. Symlinks anywhere in the
/// source are rejected; a stale `.installed-from` marker is never copied so
/// provenance always reflects *this* install.
pub(super) fn stage_local_copy(
    source: &Path,
    user_plugins_dir: &Path,
    max_size: u64,
) -> Result<StagedPlugin> {
    // Validate the source first; this also rejects symlinked roots/manifests.
    let manifest_path =
        crate::plugins::agent_plugin::resolve_manifest_path(source).ok_or_else(|| {
            anyhow::anyhow!(
                "source is not a valid plugin bundle: no plugin.json, .claude-plugin/plugin.json, kimi.plugin.json, or plugin.toml"
            )

View on GitHub (pinned to 73e0f67d83)