{"record":{"id":"6671d8783f47a526","repo":"Hmbown/CodeWhale","slug":"plugin-name-is-not-a-safe-directory-name-error-6671d8","errorCode":null,"errorMessage":"[plugin].name is not a safe directory name: {error:#}","messagePattern":"\\[plugin\\]\\.name is not a safe directory name: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/plugins/install/stage.rs","lineNumber":79,"sourceCode":"    fs::create_dir_all(user_plugins_dir).with_context(|| {\n        format!(\n            \"failed to create user plugins directory {}\",\n            user_plugins_dir.display()\n        )\n    })\n}\n\n/// Validate the staged tree and return the manifest name + content hash.\npub(super) fn validate_staged(staged_path: &Path) -> Result<(String, String)> {\n    let manifest_path = crate::plugins::agent_plugin::resolve_manifest_path(staged_path)\n        .ok_or_else(|| {\n            anyhow::anyhow!(\"staged bundle has no plugin.json, .claude-plugin/plugin.json, kimi.plugin.json, or plugin.toml\")\n        })?;\n    let validated = PluginManifest::validate_from_path(&manifest_path)\n        .map_err(|error| anyhow::anyhow!(\"staged plugin manifest failed validation: {error}\"))?;\n    let name = validated.manifest.plugin.name.clone();\n    validate_skill_name_segment(&name).map_err(|error| {\n        anyhow::anyhow!(\"[plugin].name is not a safe directory name: {error:#}\")\n    })?;\n    Ok((name, validated.content_hash))\n}\n\n/// Copy a local bundle directory into staging. Symlinks anywhere in the\n/// source are rejected; a stale `.installed-from` marker is never copied so\n/// provenance always reflects *this* install.\npub(super) fn stage_local_copy(\n    source: &Path,\n    user_plugins_dir: &Path,\n    max_size: u64,\n) -> Result<StagedPlugin> {\n    // Validate the source first; this also rejects symlinked roots/manifests.\n    let manifest_path =\n        crate::plugins::agent_plugin::resolve_manifest_path(source).ok_or_else(|| {\n            anyhow::anyhow!(\n                \"source is not a valid plugin bundle: no plugin.json, .claude-plugin/plugin.json, kimi.plugin.json, or plugin.toml\"\n            )","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/plugins/install/stage.rs#L61-L97","documentation":"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.","triggerScenarios":"`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.","commonSituations":"A malicious or typo'd manifest with name \"../../evil\", scoped names like \"@scope/pkg\", names with spaces/slashes, or an empty name field.","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."],"exampleFix":"// before: plugin.json\n{ \"plugin\": { \"name\": \"../../.ssh\", \"version\": \"1.0.0\" } }\n// after\n{ \"plugin\": { \"name\": \"my-plugin\", \"version\": \"1.0.0\" } }","handlingStrategy":"validation","validationCode":"const name = manifest.plugin.name;\nif (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) {\n  throw new Error(`[plugin].name not a safe directory segment: ${name}`);\n}","typeGuard":"const isSafePluginName = (n) => typeof n === 'string' && n.length > 0 &&\n  !n.includes('/') && !n.includes('\\\\') && n !== '.' && n !== '..' &&\n  /^[\\w-]+$/.test(n);","tryCatchPattern":"try {\n  await codewhale.plugins.install(srcDir);\n} catch (e) {\n  if (e.message.includes('[plugin].name is not a safe directory name')) {\n    console.error('fix the name field in the plugin manifest');\n  } else throw e;\n}","preventionTips":["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."],"tags":["plugins","path-traversal","validation","rust"],"backgroundTag":"path-traversal-blocked","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}