JuliusBrussee/caveman · error

caveman-code: path escapes the workspace

Error message

caveman-code: path escapes the workspace: ${candidate}

What it means

containedPath validates that a user-supplied path candidate stays inside the canonicalized workspace root. Both workspace and candidate are canonicalized, then escapesRoot checks the relative path; if the candidate resolves outside the root (or is an absolute path that escapes), the library throws this error to enforce sandbox containment of all file operations.

Solutions

  1. Pass paths relative to the workspace root and ensure they resolve inside it
  2. Resolve symlinks: pass the real path so canonicalization keeps it inside the workspace
  3. Set the workspace option to a parent directory that actually contains the target path

Example fix

// before
await code.run({ workspace: "/work/app" }, { path: "../secrets/key.pem" });
// after
await code.run({ workspace: "/work" }, { path: "app/src/main.ts" });
Defensive patterns

Strategy: validation

Validate before calling

import { resolve, relative, isAbsolute } from "node:path";
function staysInWorkspace(workspace: string, candidate: string): boolean {
  const rel = relative(resolve(workspace), resolve(workspace, candidate));
  return !(rel === ".." || rel.startsWith("../") || isAbsolute(rel));
}
if (!staysInWorkspace(ws, p)) throw new Error(`refusing out-of-workspace path: ${p}`);

Type guard

const isInsideWorkspace = (rel: string): boolean =>
  rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\") && !isAbsolute(rel);

Try / catch

try {
  const full = await containedPath(ws, candidate);
} catch (err) {
  if ((err as Error).message.includes("path escapes the workspace")) {
    console.error(`Rejected path ${candidate}; use a path inside the workspace`);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling any caveman-code API that takes a path (e.g. staging source files) with a path containing '../' segments that climb out of the workspace, a symlink pointing outside the workspace, or an absolute path outside the workspace.

Common situations: Passing an absolute path from elsewhere on disk while CWD differs from the workspace; a temp dir symlink on macOS (/tmp -> /private/tmp) making a seemingly-inside path canonicalize outside; using '..' to reference sibling projects.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/438996adc48b8ffc. Report an issue: GitHub.

Appendix: source

Thrown at packages/agent/src/code.ts:386

      );
    },
  });

  return [readFileTool, grepTool, bashTool, editTool];
}

/**
 * Resolve a caller path against the canonical workspace and refuse anything
 * that lands outside it.
 *
 * A lexical prefix check is not containment: a symlink inside the workspace
 * pointing anywhere on the filesystem passes it. Both sides are canonicalized
 * first, matching how `stageSandboxSourceGraph` decides the same question.
 */
async function containedPath(canonicalWorkspace: string, candidate: string): Promise<string> {
  const full = await canonicalizePath(resolve(canonicalWorkspace, candidate));
  if (escapesRoot(relative(canonicalWorkspace, full))) {
    throw new Error(`caveman-code: path escapes the workspace: ${candidate}`);
  }
  return full;
}

function escapesRoot(path: string): boolean {
  return path === ".." || path.startsWith("../") || path.startsWith("..\\") || isAbsolute(path);
}

/**
 * `realpath` for a path whose leaf may not exist yet (a file `edit_file` is
 * about to create): canonicalize the deepest existing ancestor and re-attach
 * the missing tail, so every symlink on the existing part is still resolved.
 */
async function canonicalizePath(target: string): Promise<string> {
  const missing: string[] = [];
  let current = target;
  for (;;) {
    try {

View on GitHub (pinned to 3ee70a1026)