{"record":{"id":"20b9d937f91d060c","repo":"zeroclaw-labs/zeroclaw","slug":"worktree-path-resolves-outside-the-workspace","errorCode":null,"errorMessage":"Worktree path '{}' resolves outside the workspace or allowed roots","messagePattern":"Worktree path '(.+?)' resolves outside the workspace or allowed roots","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/git_operations.rs","lineNumber":168,"sourceCode":"            ::zeroclaw_log::record!(\n                WARN,\n                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)\n                    .with_outcome(::zeroclaw_log::EventOutcome::Failure)\n                    .with_attrs(::serde_json::json!({\n                        \"parent\": parent.display().to_string(),\n                        \"error\": format!(\"{}\", e),\n                    })),\n                \"git_operations: cannot resolve worktree parent\"\n            );\n            anyhow::Error::msg(format!(\n                \"Cannot resolve worktree parent '{}': {e}\",\n                parent.display()\n            ))\n        })?;\n        let resolved_target = resolved_parent.join(file_name);\n\n        if !self.security.is_resolved_path_allowed(&resolved_target) {\n            anyhow::bail!(\n                \"Worktree path '{}' resolves outside the workspace or allowed roots\",\n                raw_path\n            );\n        }\n\n        Ok(resolved_target)\n    }\n\n    fn ensure_worktree_remove_target_allowed(&self, raw_path: &str) -> anyhow::Result<PathBuf> {\n        let candidate = self.candidate_path(raw_path)?;\n        let resolved = candidate.canonicalize().map_err(|e| {\n            ::zeroclaw_log::record!(\n                WARN,\n                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)\n                    .with_outcome(::zeroclaw_log::EventOutcome::Failure)\n                    .with_attrs(::serde_json::json!({\n                        \"raw_path\": raw_path,\n                        \"error\": format!(\"{}\", e),","sourceCodeStart":150,"sourceCodeEnd":186,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/git_operations.rs#L150-L186","documentation":"Before creating a worktree, ensure_worktree_add_target_allowed resolves the target's parent directory (which must already exist, because canonicalize fails on missing paths), re-joins the file name, and asks SecurityPolicy::is_resolved_path_allowed whether the resolved target is inside the workspace root or one of the policy's allowed_roots (git_operations.rs:165-172; policy check at zeroclaw-config/src/policy.rs:3056). If the location is outside every allowlisted root, the add is refused.","triggerScenarios":"Calling git_worktree add with a target outside the workspace, e.g. \"/tmp/wt\" or \"~/wt\" (broad default-forbidden roots include /home and /tmp per policy.rs comments); targeting a sibling directory of the workspace; targeting a path whose parent does not exist yet (fails first with 'Cannot resolve worktree parent'); running under a workspace_only security policy with no extra allowed_roots configured.","commonSituations":"Developers following the `git worktree add ../branch-x` convention inside a sandboxed agent workspace; CI sandboxes where the workspace is a subdirectory; policies tightened to workspace_only during a security review so previously fine locations stop working.","solutions":["Create worktrees under the workspace root (e.g. \".worktrees/name\") — always allowed.","Create the parent directory first so canonicalize succeeds, keeping it under the workspace.","If an external location is genuinely required, add its canonical parent directory to the SecurityPolicy allowed_roots in zeroclaw config and retry.","Pre-check with security.is_resolved_path_allowed on the caller side before issuing the add."],"exampleFix":"// before\nworktree(op: \"add\", path: \"/tmp/feature-wt\")\n// -> Worktree path '/tmp/feature-wt' resolves outside the workspace or allowed roots\n\n// after\nworktree(op: \"add\", path: \".worktrees/feature-wt\")","handlingStrategy":"validation","validationCode":"// Verify the target would pass the policy before calling the tool.\nlet candidate = workspace.join(\".worktrees\").join(name);\nstd::fs::create_dir_all(candidate.parent().unwrap())?; // parent must exist\nlet resolved_parent = candidate.parent().unwrap().canonicalize()?;\nlet target = resolved_parent.join(candidate.file_name().unwrap());\nif !security.is_resolved_path_allowed(&target) {\n    return Err(format!(\"worktree target {target:?} outside allowed roots\"));\n}","typeGuard":"fn is_allowed_worktree_target(target: &std::path::Path, security: &SecurityPolicy) -> bool {\n    security.is_resolved_path_allowed(target)\n}","tryCatchPattern":"match git_tool.execute(params).await {\n    Err(e) if e.to_string().contains(\"resolves outside the workspace or allowed roots\") => {\n        // create the worktree under the workspace instead and retry; widen\n        // allowed_roots only as a deliberate, reviewed config change\n    }\n    r => r,\n}","preventionTips":["Standardize on a .worktrees/ directory inside the workspace","Create the parent directory before requesting worktree add","Keep SecurityPolicy allowed_roots minimal and explicit","Pre-check targets with security.is_resolved_path_allowed when you hold the policy handle"],"tags":["git","worktree","security","allowlist","workspace","path"],"backgroundTag":"path-allowlist-denied","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}