Hmbown/CodeWhale · error
plugin archive bundle path must contain only safe relative…
Error message
plugin archive bundle path must contain only safe relative directory names
What it means
`remote_bundle_path` validates the bundle path inside a plugin archive tarball before extracting it. Every path component must be a plain relative directory/file name of ASCII alphanumerics, `-`, `_`, or `.` only — no absolute paths, `..` traversal, separators, or other characters. The library bails here to prevent archive members from escaping the install directory (zip-slip).
Solutions
- Repackage the archive so the bundle sits under a flat relative directory using only [A-Za-z0-9._-] in every component
- Verify member paths with `tar -tf bundle.tar.gz` and fix any absolute or `..` entries
- If the source is your own CI artifact, change the packaging step (e.g. `tar -czf bundle.tar.gz -C dist .`)
- If you do not control the archive, download and inspect it manually instead of using remote install
Example fix
// before: members like "/abs/path/plugin.wasm" or "../plugin.wasm" tar -czf bundle.tar.gz ./plugin // after: flat, relative, safe names tar -czf bundle.tar.gz -C dist plugin.wasm
Defensive patterns
Strategy: validation
Validate before calling
fn is_safe_bundle_path(path: &str) -> bool {
!path.starts_with('/')
&& path.split('/').all(|p| {
!p.is_empty() && p != "." && p != ".."
&& p.bytes().all(|c| c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'.'))
})
}
// call before handing the tarball to install_remote_bytes Try / catch
match install_remote_bytes(bytes) {
Err(e) if e.to_string().contains("safe relative directory names") => {
eprintln!("archive layout rejected; repackage with flat relative paths")
}
other => other?,
} Prevention
- Inspect tarball members (`tar -tf`) before publishing
- Package with `-C dist .` so paths stay relative and flat
- Never include absolute paths or `..` in archive entries
When it happens
Trigger: Installing a remote plugin archive whose internal bundle path contains an absolute component (leading `/`), a `..` component, an empty component, or characters outside `[A-Za-z0-9._-]` (e.g. spaces, unicode, `+`). Raised from `parse` / `install_remote_bytes` when processing the downloaded tarball.
Common situations: A plugin author packaged the tarball with nested folders like `my plugin/dist` or `./build/../plugin`; a build tool emitted absolute member paths; a malicious or misconfigured archive contains traversal entries.
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 path escapes plugins directory
- audited skill path does not match owned package
- built-in plugin path may not be a symbolic link or reparse…
- bundle path escapes the config directory via a symlink…
- bundle path is absolute; only paths inside the config…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/b14ad2bcc2dda788.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/plugins/install/mod.rs:165
};
let url = reqwest::Url::parse(raw).context("invalid plugin archive URL")?;
let Some(fragment) = url.fragment() else {
return Ok(None);
};
let path = fragment
.strip_prefix("path=")
.context("plugin archive fragment must be #path=<bundle-directory>")?;
if path.is_empty()
|| !path.split('/').all(|part| {
!part.is_empty()
&& part != "."
&& part != ".."
&& part
.bytes()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, b'-' | b'_' | b'.'))
})
{
bail!("plugin archive bundle path must contain only safe relative directory names");
}
Ok(Some(path.to_string()))
}
/// Serialize a source for the `.installed-from` marker. Must round-trip
/// through [`PluginInstallSource::parse`].
fn plugin_spec_string(source: &PluginInstallSource, canonical_source: Option<&Path>) -> String {
match source {
PluginInstallSource::LocalPath(_) => {
let path = canonical_source.expect("local installs record the canonical source");
format!("path:{}", path.display())
}
PluginInstallSource::Remote(remote) => source_spec_string(remote),
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Outcome / result typesView on GitHub (pinned to 73e0f67d83)