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

Before deriving a plugin's target directory from its name, the name is run through `validate_skill_name_segment`. If the name contains path separators, dot segments, or other unsafe characters, the operation is refused to prevent escaping the plugins directory (path traversal). The name is used verbatim as a path segment, so it must be a safe single directory name.

Solutions

  1. Use the plain plugin directory name without slashes, dots-prefixes, or path components.
  2. Run the uninstall/update against the installed directory listing to find the correct name (e.g. check ~/.codewhale/plugins/).
  3. Fix the plugin's manifest `[plugin].name` to a safe identifier and reinstall.
  4. If the name comes from user input in a script, validate it before passing it in.

Example fix

// before
codewhale plugin uninstall "@scope/my-plugin"
// after
codewhale plugin uninstall "my-plugin"
Defensive patterns

Strategy: validation

Validate before calling

function isSafeNameSegment(name) {
  return typeof name === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) &&
    !name.includes('/') && name !== '.' && name !== '..';
}
if (!isSafeNameSegment(pluginName)) throw new Error(`unsafe plugin name: ${pluginName}`);

Type guard

const isPlainSlug = (v) => typeof v === 'string' && v.length > 0 && /^[\w-]+$/.test(v);

Try / catch

try {
  await codewhale.plugins.uninstall(name);
} catch (e) {
  if (e.message.includes('not a safe directory name')) {
    console.error(`invalid plugin name "${name}" — use the bare slug`);
  } else throw e;
}

Prevention

When it happens

Trigger: `plugin_target_path(name, user_plugins_dir)` (called by `update` and `uninstall`) receives a plugin name like `../foo`, `a/b`, `..`, or an empty/invalid segment.

Common situations: Pasting a scoped npm-style name (e.g. `@scope/pkg`) or a URL/slug with slashes into update/uninstall, a manifest name containing `/` or `..`, scripting the CLI with untrusted names.

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/6c8924608743a10e. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/plugins/install/place.rs:125

        fs::remove_dir_all(&backup).ok();
    }

    Ok(PluginInstallOutcome::Installed(InstalledPlugin {
        name: staged.name,
        path: final_path,
        content_hash: staged.content_hash,
        installed_content_hash,
        source_checksum: source_checksum.to_string(),
    }))
}

// ─────────────────────────────────────────────────────────────────────────────
// Path guards
// ─────────────────────────────────────────────────────────────────────────────

pub(super) fn plugin_target_path(name: &str, user_plugins_dir: &Path) -> Result<PathBuf> {
    let name = validate_skill_name_segment(name)
        .map_err(|error| anyhow::anyhow!("plugin name is not a safe directory name: {error:#}"))?;
    Ok(user_plugins_dir.join(name))
}

/// The resolved bundle must be a direct child of the resolved plugins root,
/// matching discovery's fail-closed containment rule.
pub(super) fn ensure_target_within_plugins_dir(
    target: &Path,
    user_plugins_dir: &Path,
) -> Result<()> {
    let root = fs::canonicalize(user_plugins_dir).with_context(|| {
        format!(
            "failed to resolve plugins directory {}",
            user_plugins_dir.display()
        )
    })?;
    let target = fs::canonicalize(target)
        .with_context(|| format!("failed to resolve {}", target.display()))?;
    if target.parent() != Some(root.as_path()) {

View on GitHub (pinned to 73e0f67d83)