ultraworkers/claw-code · error · std::io::Error

path {} escapes workspace boundary {}

Error message

path {} escapes workspace boundary {}

What it means

`validate_workspace_boundary` (runtime/src/file_ops.rs:44) enforces the workspace-containment invariant for the `*_in_workspace` file operations: the resolved path must have the workspace root as a Path-component prefix (`Path::starts_with` is component-based, so `spacex` does NOT sneak past root `space`). Violations return `ErrorKind::PermissionDenied`. It is called from grep/glob/search and the in-workspace write variants (file_ops.rs:325, 348, 361, 407, 436, 686, 699, 714).

Source

Thrown at rust/crates/runtime/src/file_ops.rs:44

];

/// Check whether a file appears to contain binary content by examining
/// the first chunk for NUL bytes.
fn is_binary_file(path: &Path) -> io::Result<bool> {
    use std::io::Read;
    let mut file = fs::File::open(path)?;
    let mut buffer = [0u8; 8192];
    let bytes_read = file.read(&mut buffer)?;
    Ok(buffer[..bytes_read].contains(&0))
}

/// Validate that a resolved path stays within the given workspace root.
/// Returns the canonical path on success, or an error if the path escapes
/// the workspace boundary (e.g. via `../` traversal or symlink).
#[allow(dead_code)]
fn validate_workspace_boundary(resolved: &Path, workspace_root: &Path) -> io::Result<()> {
    if !resolved.starts_with(workspace_root) {
        return Err(io::Error::new(
            io::ErrorKind::PermissionDenied,
            format!(
                "path {} escapes workspace boundary {}",
                resolved.display(),
                workspace_root.display()
            ),
        ));
    }
    Ok(())
}

/// Text payload returned by file-reading operations.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TextFilePayload {
    #[serde(rename = "filePath")]
    pub file_path: String,
    pub content: String,
    #[serde(rename = "numLines")]

View on GitHub (pinned to 08106b0c37)

Solutions

  1. Keep every target path inside the workspace root; drop `../` segments from patterns.
  2. Replace out-of-boundary symlinks with copies, or move the referenced file inside the workspace.
  3. When calling the `_in_workspace` APIs programmatically, canonicalize the root (fs::canonicalize) before passing it so prefix comparison matches canonicalized targets.

Example fix

// before
let root = Path::new("~/proj");                       // tilde never expands -> every check fails
write_file_in_workspace("/etc/app.conf", data, root)?; // path ... escapes workspace boundary ...

// after
let root = std::fs::canonicalize(shellexpand_home("~/proj"))?;
let inside = root.join("config/app.conf");
write_file_in_workspace(&inside.to_string_lossy(), data, &root)?;
Defensive patterns

Strategy: validation

Validate before calling

fn inside_workspace(target: &Path, root: &Path) -> io::Result<bool> {
    let root = std::fs::canonicalize(root)?;
    let target = std::fs::canonicalize(target)?;
    Ok(target.starts_with(&root))   // component-wise prefix, like the guard
}

Try / catch

match write_file_in_workspace(path, data, root) {
    Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied
        && e.to_string().contains("escapes workspace boundary") => { /* reject pattern / move file inside root */ }
    other => other,
}

Prevention

When it happens

Trigger: A glob or search pattern containing `../` that resolves outside the root; a symlink inside the workspace pointing to a directory outside it; passing an absolute path in `/tmp` or `$HOME` to an in-workspace op; the workspace root itself being non-canonical (given root `~/proj` while resolved paths canonicalize through a symlink to `/home/user/proj`).

Common situations: Monorepos where `node_modules` symlinks point to global package stores; `bun`/`pnpm` symlink farms; configs referencing files outside the project; running claw from a symlinked path so the passed root never prefix-matches canonicalized targets.

Related errors


AI-assisted analysis of ultraworkers/claw-code@08106b0c37 (2026-08-18). Data as JSON: /api/errors/bd4ce21d994126c5. Report an issue: GitHub.