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
- Add the workspace's resolved absolute path (or a parent of it) to runtime.docker.allowed_workspace_roots.
- Make the allowlist entry match the resolved/canonical form — no symlinks — since the comparison happens after resolution.
- Re-run and compare the path printed in the error against your allowlist entries to spot the spelling mismatch.
- 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
- Store allowlist entries in canonical (symlink-free) form; comparison happens after resolution.
- Re-verify the allowlist whenever you move or add workspace directories.
- Include every mount location the workflow uses (project dir, tmp scratch dirs).
- Treat the error's printed path as authoritative when reconciling the allowlist.
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
- Refusing to mount filesystem root (/) into docker runtime
- Worktree path '{}' resolves outside the workspace or allowed
- Lark/Feishu marker target resolves outside workspace_dir
- Docker runtime requires an absolute workspace path, got: {}
- Serial path not allowed: {}. Allowed: {}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/70029796193e8a08.
Report an issue: GitHub.