different-ai/openwork · error · ApiError

path_escape

path_escape

Error message

Path escapes workspace root

What it means

resolveWithinRoot resolves requested segments under the workspace root, resolves symlinks via realpath, and throws this 400 ApiError (code path_escape) when the resolved candidate lands outside the root. It prevents path traversal (../) and symlink escape from reading/writing outside the workspace.

Source

Thrown at apps/server/src/paths.ts:17

import { realpath } from "node:fs/promises";
import { isAbsolute, resolve, sep } from "node:path";
import { ApiError } from "./errors.js";

export function assertAbsolute(path: string): void {
  if (!isAbsolute(path)) {
    throw new ApiError(400, "invalid_path", "Path must be absolute");
  }
}

export async function resolveWithinRoot(root: string, ...segments: string[]): Promise<string> {
  const resolvedRoot = await realpath(root);
  const candidate = resolve(resolvedRoot, ...segments);
  const resolvedCandidate = await realpath(candidate).catch(() => candidate);
  if (resolvedCandidate === resolvedRoot) return candidate;
  if (!resolvedCandidate.startsWith(resolvedRoot + sep)) {
    throw new ApiError(400, "path_escape", "Path escapes workspace root");
  }
  return candidate;
}

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Remove ../ segments and pass paths that stay inside the workspace root.
  2. Remove or relocate symlinks that point outside the workspace.
  3. Normalize/canonicalize the requested path client-side and reject escapes before calling.
  4. If external files are legitimately needed, copy or mount them inside the workspace rather than linking.

Example fix

// before: unsanitized user input
await api.filePath({ path: resolve(root, userInput) }); // userInput = "../../etc/passwd"
// after: validate containment first
const candidate = resolve(root, userInput);
if (!candidate.startsWith(root + sep)) throw new ApiError(400, "invalid_path", "Path must stay inside workspace");
await api.filePath({ path: candidate });
Defensive patterns

Strategy: validation

Validate before calling

import { resolve, sep, isAbsolute } from "node:path";
function staysWithinRoot(root: string, ...segments: string[]): boolean {
  const candidate = resolve(root, ...segments);
  return candidate === root || candidate.startsWith(root + sep);
}

Type guard

function isInsideWorkspace(root: string, p: string): boolean {
  if (!isAbsolute(p)) return false;
  const rel = relative(root, p);
  return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
}

Try / catch

import { ApiError } from "./errors.js";
try {
  const path = await resolveWithinRoot(root, userInput);
} catch (err) {
  if (err instanceof ApiError && err.code === "path_escape") {
    throw new ApiError(400, "path_escape", "Requested path is outside the workspace and was blocked.");
  }
  throw err;
}

Prevention

When it happens

Trigger: filePath (or another resolveWithinRoot caller) invoked with segments containing ../ that escape the root, or a symlink inside the workspace pointing to an external target.

Common situations: Path traversal attempts on the attachments/file API; symlinks created inside workspaces pointing at home dirs; clients joining user input into paths without normalization.

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 different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/6b68bfe09812cc24. Report an issue: GitHub.