{"record":{"id":"6c8924608743a10e","repo":"Hmbown/CodeWhale","slug":"plugin-name-is-not-a-safe-directory-name-error","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/place.rs","lineNumber":125,"sourceCode":"        fs::remove_dir_all(&backup).ok();\n    }\n\n    Ok(PluginInstallOutcome::Installed(InstalledPlugin {\n        name: staged.name,\n        path: final_path,\n        content_hash: staged.content_hash,\n        installed_content_hash,\n        source_checksum: source_checksum.to_string(),\n    }))\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Path guards\n// ─────────────────────────────────────────────────────────────────────────────\n\npub(super) fn plugin_target_path(name: &str, user_plugins_dir: &Path) -> Result<PathBuf> {\n    let name = validate_skill_name_segment(name)\n        .map_err(|error| anyhow::anyhow!(\"plugin name is not a safe directory name: {error:#}\"))?;\n    Ok(user_plugins_dir.join(name))\n}\n\n/// The resolved bundle must be a direct child of the resolved plugins root,\n/// matching discovery's fail-closed containment rule.\npub(super) fn ensure_target_within_plugins_dir(\n    target: &Path,\n    user_plugins_dir: &Path,\n) -> Result<()> {\n    let root = fs::canonicalize(user_plugins_dir).with_context(|| {\n        format!(\n            \"failed to resolve plugins directory {}\",\n            user_plugins_dir.display()\n        )\n    })?;\n    let target = fs::canonicalize(target)\n        .with_context(|| format!(\"failed to resolve {}\", target.display()))?;\n    if target.parent() != Some(root.as_path()) {","sourceCodeStart":107,"sourceCodeEnd":143,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/plugins/install/place.rs#L107-L143","documentation":"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.","triggerScenarios":"`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.","commonSituations":"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.","solutions":["Use the plain plugin directory name without slashes, dots-prefixes, or path components.","Run the uninstall/update against the installed directory listing to find the correct name (e.g. check ~/.codewhale/plugins/).","Fix the plugin's manifest `[plugin].name` to a safe identifier and reinstall.","If the name comes from user input in a script, validate it before passing it in."],"exampleFix":"// before\ncodewhale plugin uninstall \"@scope/my-plugin\"\n// after\ncodewhale plugin uninstall \"my-plugin\"","handlingStrategy":"validation","validationCode":"function isSafeNameSegment(name) {\n  return typeof name === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) &&\n    !name.includes('/') && name !== '.' && name !== '..';\n}\nif (!isSafeNameSegment(pluginName)) throw new Error(`unsafe plugin name: ${pluginName}`);","typeGuard":"const isPlainSlug = (v) => typeof v === 'string' && v.length > 0 && /^[\\w-]+$/.test(v);","tryCatchPattern":"try {\n  await codewhale.plugins.uninstall(name);\n} catch (e) {\n  if (e.message.includes('not a safe directory name')) {\n    console.error(`invalid plugin name \"${name}\" — use the bare slug`);\n  } else throw e;\n}","preventionTips":["Pass only the bare plugin slug to update/uninstall — no scopes, slashes, or paths.","Keep manifest names as simple identifiers (letters/digits/hyphens).","Sanitize any name taken from user input or untrusted manifests before calling the CLI."],"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"}