zeroclaw-labs/zeroclaw · error · anyhow::Error

Workspace path {} is not in runtime.docker.allowed_workspace

Error message

Workspace path {} is not in runtime.docker.allowed_workspace_roots

What it means

When runtime.docker.allowed_workspace_roots is non-empty it acts as an allowlist: the resolved workspace path must lexically start with one of the resolved roots or the mount is refused. An empty list disables the allowlist entirely. This is the sandbox boundary keeping the container bind mount inside directories you explicitly trusted.

Source

Thrown at crates/zeroclaw-config/src/platform/docker.rs:76

        }

        let allowed_roots = self
            .config
            .allowed_workspace_roots
            .iter()
            .map(|root| {
                Path::new(root).canonicalize().map_err(|source| {
                    DockerWorkspaceMountError::AllowedRoot {
                        path: root.clone(),
                        source,
                    }
                })
            })
            .collect::<std::result::Result<Vec<_>, _>>()?;
        let allowed = allowed_roots.iter().any(|root| resolved.starts_with(root));

        if !allowed {
            anyhow::bail!(
                "Workspace path {} is not in runtime.docker.allowed_workspace_roots",
                resolved.display()
            );
        }

        Ok(resolved)
    }

    fn build_shell_command_inner(
        &self,
        command: &str,
        workspace_dir: &Path,
        env_keys: &[&OsStr],
    ) -> anyhow::Result<tokio::process::Command> {
        let mut process = tokio::process::Command::new("docker");
        process
            .arg("run")
            .arg("--rm")

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Add the workspace's resolved absolute path (or a parent of it) to runtime.docker.allowed_workspace_roots.
  2. Make the allowlist entry match the resolved/canonical form — no symlinks — since the comparison happens after resolution.
  3. Re-run and compare the path printed in the error against your allowlist entries to spot the spelling mismatch.
  4. Leaving allowed_workspace_roots empty disables the sandbox check, but only do that with other isolation in place.

Example fix

# before
[runtime.docker]
allowed_workspace_roots = ["/home/me"]
# workspace at /tmp/zeroclaw → refused

# after
[runtime.docker]
allowed_workspace_roots = ["/home/me", "/tmp/zeroclaw"]
Defensive patterns

Strategy: validation

Validate before calling

// mirror the runtime's check: compare resolved paths lexically
let resolved = std::fs::canonicalize(&workspace_dir)?;
let ok = allowed_roots.is_empty()
    || allowed_roots.iter().any(|r| resolved.starts_with(r));
if !ok { /* add the path (or a parent) to allowed_workspace_roots */ }

Try / catch

match docker_runtime.build_shell_command_inner(cmd) {
    Err(e) if e.to_string().contains("allowed_workspace_roots") => {
        // copy the path from the error message into the allowlist, canonicalized
    }
    other => other,
}

Prevention

When it happens

Trigger: allowed_workspace_roots = ["/home/me"] with a workspace like /tmp/proj or /home/other/proj. Also fires when an allowlist entry is stored as a symlink path while the workspace arrives canonicalized: the starts_with comparison is lexical on resolved paths, so equivalent-but-differently-spelled paths fail.

Common situations: Configuring the allowlist for the home directory then running zeroclaw from /tmp or another mount; roots recorded before canonicalization; moving the workspace after the allowlist was set.

Related errors


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