{"record":{"id":"70029796193e8a08","repo":"zeroclaw-labs/zeroclaw","slug":"workspace-path-is-not-in-runtime-docker-allowed","errorCode":null,"errorMessage":"Workspace path {} is not in runtime.docker.allowed_workspace_roots","messagePattern":"Workspace path (.+?) is not in runtime\\.docker\\.allowed_workspace_roots","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-config/src/platform/docker.rs","lineNumber":76,"sourceCode":"        }\n\n        let allowed_roots = self\n            .config\n            .allowed_workspace_roots\n            .iter()\n            .map(|root| {\n                Path::new(root).canonicalize().map_err(|source| {\n                    DockerWorkspaceMountError::AllowedRoot {\n                        path: root.clone(),\n                        source,\n                    }\n                })\n            })\n            .collect::<std::result::Result<Vec<_>, _>>()?;\n        let allowed = allowed_roots.iter().any(|root| resolved.starts_with(root));\n\n        if !allowed {\n            anyhow::bail!(\n                \"Workspace path {} is not in runtime.docker.allowed_workspace_roots\",\n                resolved.display()\n            );\n        }\n\n        Ok(resolved)\n    }\n\n    fn build_shell_command_inner(\n        &self,\n        command: &str,\n        workspace_dir: &Path,\n        env_keys: &[&OsStr],\n    ) -> anyhow::Result<tokio::process::Command> {\n        let mut process = tokio::process::Command::new(\"docker\");\n        process\n            .arg(\"run\")\n            .arg(\"--rm\")","sourceCodeStart":58,"sourceCodeEnd":94,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-config/src/platform/docker.rs#L58-L94","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\n[runtime.docker]\nallowed_workspace_roots = [\"/home/me\"]\n# workspace at /tmp/zeroclaw → refused\n\n# after\n[runtime.docker]\nallowed_workspace_roots = [\"/home/me\", \"/tmp/zeroclaw\"]","handlingStrategy":"validation","validationCode":"// mirror the runtime's check: compare resolved paths lexically\nlet resolved = std::fs::canonicalize(&workspace_dir)?;\nlet ok = allowed_roots.is_empty()\n    || allowed_roots.iter().any(|r| resolved.starts_with(r));\nif !ok { /* add the path (or a parent) to allowed_workspace_roots */ }","typeGuard":null,"tryCatchPattern":"match docker_runtime.build_shell_command_inner(cmd) {\n    Err(e) if e.to_string().contains(\"allowed_workspace_roots\") => {\n        // copy the path from the error message into the allowlist, canonicalized\n    }\n    other => other,\n}","preventionTips":["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."],"tags":["docker","security","allowlist","workspace"],"backgroundTag":"docker-workspace-allowlist-denied","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}