{"record":{"id":"4e4a966c1e51f482","repo":"zeroclaw-labs/zeroclaw","slug":"path-not-allowed-parent-directory-traversal-is-no","errorCode":null,"errorMessage":"Path not allowed: parent-directory traversal is not allowed","messagePattern":"Path not allowed: parent-directory traversal is not allowed","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/git_operations.rs","lineNumber":116,"sourceCode":"                if !resolved.starts_with(&workspace_canonical) {\n                    anyhow::bail!(\"Path '{}' resolves outside the workspace directory\", p);\n                }\n                resolved\n            }\n            _ => self.workspace_dir.clone(),\n        };\n        Ok(base)\n    }\n\n    fn candidate_path(&self, raw_path: &str) -> anyhow::Result<PathBuf> {\n        if raw_path.contains('\\0') {\n            anyhow::bail!(\"Path not allowed: contains null byte\");\n        }\n        if Path::new(raw_path)\n            .components()\n            .any(|c| matches!(c, std::path::Component::ParentDir))\n        {\n            anyhow::bail!(\"Path not allowed: parent-directory traversal is not allowed\");\n        }\n\n        let raw = Path::new(raw_path);\n        Ok(if raw.is_absolute() {\n            raw.to_path_buf()\n        } else {\n            self.workspace_dir.join(raw)\n        })\n    }\n\n    fn ensure_worktree_add_target_allowed(&self, raw_path: &str) -> anyhow::Result<PathBuf> {\n        let candidate = self.candidate_path(raw_path)?;\n        let parent = candidate.parent().ok_or_else(|| {\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!({\"raw_path\": raw_path})),","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/git_operations.rs#L98-L134","documentation":"candidate_path rejects worktree paths whose component list contains Component::ParentDir, i.e. any `..` segment (git_operations.rs:112-117). Unlike resolve_working_dir (which canonicalizes then compares), this check is lexical and fires before the path is joined onto workspace_dir, so even `..` sequences that would re-enter the workspace are refused for worktree targets.","triggerScenarios":"Calling git worktree add/remove with paths like \"../shared/wt\", \"logs/../../escape\", or any absolute path containing a .. component. Any ParentDir component anywhere in raw_path triggers the bail before candidate_path builds the joined PathBuf at git_operations.rs:119-124.","commonSituations":"Agents following the common git habit `git worktree add ../feature-x`; paths assembled by concatenating a base dir with user input without normalization; toolchains that emit ..-relative paths when no explicit target is given.","solutions":["Use a target directory inside the workspace, e.g. \".worktrees/feature-x\".","If the path came from joining inputs, normalize it first (Component-based rebuild or path-clean) so no `..` remains.","Restructure the workflow: create the worktree under the workspace and only symlink it outward if policy allows."],"exampleFix":"// before\nworktree(op: \"add\", path: \"../feature-x\")\n// -> Path not allowed: parent-directory traversal is not allowed\n\n// after\nworktree(op: \"add\", path: \".worktrees/feature-x\")","handlingStrategy":"validation","validationCode":"use std::path::{Path, Component};\nfn has_parent_traversal(raw: &str) -> bool {\n    Path::new(raw).components()\n        .any(|c| matches!(c, Component::ParentDir))\n}\nif has_parent_traversal(worktree_path) {\n    return Err(\"worktree path must not contain '..'\".into());\n}","typeGuard":"fn is_traversal_free(raw: &str) -> bool {\n    !std::path::Path::new(raw).components()\n        .any(|c| matches!(c, std::path::Component::ParentDir))\n}","tryCatchPattern":"match git_tool.execute(params).await {\n    Err(e) if e.to_string().contains(\"parent-directory traversal\") => {\n        // normalize the path (drop '..' segments lexically) and retry with\n        // the workspace-relative form — only if it denotes a location inside\n        // the workspace; otherwise report\n    }\n    r => r,\n}","preventionTips":["Assemble worktree targets by joining the workspace root with a single name component","Normalize user input before passing paths to tools","Remember the check is lexical: even in-workspace .. re-entries are refused"],"tags":["git","worktree","path-traversal","security","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}