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
- Change the manifest's [plugin].name to a simple identifier: letters, digits, hyphens, underscores only.
- Remove path separators or '..' from the name; use the bare plugin slug.
- If the name comes from a template generator, fix the generator's slugification step.
- 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
- Generate names with a slugifier; never embed raw user/registry names in manifests.
- Reject names containing '/', '\\', '..', or '@scope/' prefixes at authoring time.
- Validate the manifest name in CI before publishing the bundle.
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
- plugin name is not a safe directory name
- A pinned task provider requires an explicit model
- agent profile provider cannot be empty
- agent profile provider must be a simple provider id
- Antigravity cloud-code request has no text contents
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)