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

  1. Copy the real executable into the plugin directory and point `binary` at the copy
  2. Remove out-of-tree symlinks so the binary is a real file under the plugin dir
  3. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/4e7bc2b632abf4ec. Report an issue: GitHub.