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
- Remove ../ segments and pass paths that stay inside the workspace root.
- Remove or relocate symlinks that point outside the workspace.
- Normalize/canonicalize the requested path client-side and reject escapes before calling.
- 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
- Normalize user-supplied path segments (strip .., resolve) before sending to the API.
- Avoid creating symlinks inside workspaces that point to external locations.
- Never join raw user input into paths; route through a containment check first.
- Treat path_escape from untrusted clients as a traversal attempt — log it.
- Remember realpath is applied: even valid-looking paths escape if a symlink target is outside the root.
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
- path_escape
- Archive contains an unsafe path
- External URL protocol "${parsed.protocol}" is not allowed.
- Workspace path is unavailable; attachments could not be copi
- plugin_manifest_not_found
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/6b68bfe09812cc24.
Report an issue: GitHub.