zeroclaw-labs/zeroclaw · error
Path '{}' resolves outside the workspace directory
Error message
Path '{}' resolves outside the workspace directory What it means
resolve_working_dir canonicalizes the caller-supplied working directory and requires the result to be a prefix-descendant of the tool's workspace_dir (git_operations.rs:94-100). If the canonicalized path does not start with the canonical workspace root, the request is rejected — this stops both literal ../ traversal and symlink escapes, because canonicalize resolves symlinks before the comparison.
Source
Thrown at crates/zeroclaw-tools/src/git_operations.rs:99
let resolved = candidate.canonicalize().map_err(|e| {
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
.with_outcome(::zeroclaw_log::EventOutcome::Failure)
.with_attrs(::serde_json::json!({
"path": p,
"error": format!("{}", e),
})),
"git_operations: cannot resolve path"
);
anyhow::Error::msg(format!("Cannot resolve path '{}': {}", p, e))
})?;
let workspace_canonical = self
.workspace_dir
.canonicalize()
.unwrap_or_else(|_| self.workspace_dir.clone());
if !resolved.starts_with(&workspace_canonical) {
anyhow::bail!("Path '{}' resolves outside the workspace directory", p);
}
resolved
}
_ => self.workspace_dir.clone(),
};
Ok(base)
}
fn candidate_path(&self, raw_path: &str) -> anyhow::Result<PathBuf> {
if raw_path.contains('\0') {
anyhow::bail!("Path not allowed: contains null byte");
}
if Path::new(raw_path)
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
anyhow::bail!("Path not allowed: parent-directory traversal is not allowed");
}View on GitHub (pinned to 88bb9c8533)
Solutions
- Use a path relative to the workspace root (e.g. "crates/tools") instead of ../ or absolute paths.
- If the target must be usable, reconstruct GitOperationsTool::new with workspace_dir set to a legitimate common ancestor containing it.
- If the escape is via a symlink inside the workspace, remove the symlink or place the real directory under the workspace.
- Pre-check on the caller side with std::fs::canonicalize and starts_with(workspace_dir) before invoking the tool.
Example fix
// before git_execute(op: "status", working_dir: "../other-project") // -> Path '../other-project' resolves outside the workspace directory // after git_execute(op: "status", working_dir: "other-project") // where other-project is a subdirectory of the configured workspace
Defensive patterns
Strategy: validation
Validate before calling
// Reproduce the tool's check before invoking.
fn resolves_inside_workspace(raw: Option<&str>, workspace: &std::path::Path) -> std::io::Result<bool> {
let Some(p) = raw.filter(|s| !s.is_empty()) else { return Ok(true); };
let cand = if std::path::Path::new(p).is_absolute() {
std::path::PathBuf::from(p)
} else {
workspace.join(p)
};
let resolved = cand.canonicalize()?;
let ws = workspace.canonicalize().unwrap_or_else(|_| workspace.to_path_buf());
Ok(resolved.starts_with(&ws))
}
if !resolves_inside_workspace(working_dir, &workspace)? { /* fix the path or abort */ } Type guard
fn is_workspace_relative(p: &str) -> bool {
!p.contains("..") && !std::path::Path::new(p).is_absolute()
} Try / catch
match git_tool.execute(params).await {
Err(e) if e.to_string().contains("resolves outside the workspace directory") => {
// recompute the path relative to the workspace root (strip the
// common prefix) and retry once; a second failure means the target
// is genuinely out of scope — do not loop
}
r => r,
} Prevention
- Always pass working_dir relative to the workspace root
- Avoid symlinks inside the workspace that point outside it
- Construct GitOperationsTool with the widest legitimate workspace_dir up front
- Never build working_dir from raw user input without normalization
When it happens
Trigger: Passing working_dir="../sibling-repo" or any absolute path outside the workspace to the git tool's execute; passing a path inside the workspace that is a symlink whose target lives outside (canonicalize returns the outer target, which fails starts_with); passing a path whose parent symlink points into /tmp or /home.
Common situations: Agents that know a sibling checkout exists and try to operate on it; configs where workspace_dir was set to a subdirectory while the caller assumes the parent; workspaces containing convenience symlinks (shared caches, node_modules-style links) that escape the root; absolute paths pasted from a terminal.
Related errors
- Worktree path '{}' resolves outside the workspace or allowed
- Lark/Feishu marker target resolves outside workspace_dir
- Slack outbound attachment path must be absolute: {target}
- Slack outbound attachment path escapes workspace: {}
- attachment path {} canonicalizes to {} which escapes workspa
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/c3beda110b623a93.
Report an issue: GitHub.