zeroclaw-labs/zeroclaw · error
manifest exec binary escapes plugin directory: {} is not und
Error message
manifest exec binary escapes plugin directory: {} is not under {} What it means
After canonicalizing both paths, `load_one_plugin` enforces that the executable resolves inside the plugin directory; a `../` path or a symlink whose target lives outside the plugin dir makes containment fail and the manifest is rejected. This is a deliberate security guard: a plugin manifest must not be able to execute arbitrary files outside its own folder. The message prints both the canonical binary path and the canonical plugin dir for comparison.
Source
Thrown at crates/zeroclaw-hardware/src/loader.rs:229
}
let binary_path = raw_binary_path.canonicalize().map_err(|e| {
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
.with_outcome(::zeroclaw_log::EventOutcome::Failure)
.with_attrs(::serde_json::json!({
"binary_path": raw_binary_path.display().to_string(),
"error": format!("{}", e),
})),
"cannot canonicalize plugin binary path"
);
anyhow::Error::msg(format!(
"cannot canonicalize binary path {}: {e}",
raw_binary_path.display()
))
})?;
if !binary_path.starts_with(&canonical_plugin_dir) {
anyhow::bail!(
"manifest exec binary escapes plugin directory: {} is not under {}",
binary_path.display().to_string(),
canonical_plugin_dir.display()
);
}
if !binary_path.is_file() {
anyhow::bail!(
"manifest exec binary is not a regular file: {}",
binary_path.display()
);
}
let name = manifest.tool.name.clone();
let version = manifest.tool.version.clone();
let tool: Box<dyn Tool> = Box::new(SubprocessTool::new(manifest, binary_path));
Ok(LoadedPlugin {
name,View on GitHub (pinned to 88bb9c8533)
Solutions
- Copy the real executable into the plugin directory and point `binary` at the copy
- Remove out-of-tree symlinks so the binary is a real file under the plugin dir
- Never author `../` paths in `binary` — they are rejected by design
Example fix
# before [exec] binary = "../../../usr/local/bin/my-tool" # after — vendored copy inside the plugin dir [exec] binary = "bin/my-tool"
Defensive patterns
Strategy: validation
Validate before calling
let canonical_dir = plugin_dir.canonicalize()?;
let binary = plugin_dir.join(&manifest.exec.binary);
if binary.exists() {
let canonical_binary = binary.canonicalize()?;
if !canonical_binary.starts_with(&canonical_dir) {
anyhow::bail!("binary escapes plugin dir — vendors must ship inside the plugin");
}
} Prevention
- Vendor every plugin executable inside the plugin directory; never reference system binaries
- Avoid symlinks in plugin dirs — canonicalize resolves them and containment fails
- Run the same containment check in your packaging step so bad plugins never reach the loader
When it happens
Trigger: `binary = "../../usr/bin/curl"`; `binary` naming a symlink inside the plugin dir whose target is elsewhere (canonicalize resolves it); unusual dev setups where the plugin dir itself is only reachable through a symlink so canonical prefixes diverge.
Common situations: Trying to reuse a system binary from a plugin; symlinks created by package managers; plugin directories relocated across mounts.
Related errors
- manifest missing [tool] name
- manifest missing [tool] description
- manifest missing [exec] binary
- manifest exec binary not found: {}
- manifest exec binary is not a regular file: {}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/4e7bc2b632abf4ec.
Report an issue: GitHub.