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
- 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.
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
- 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.
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
- [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/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)