mastra-ai/mastra · error
Path is outside the workspace
Error message
Path is outside the workspace
What it means
confinedWorkspaceRelativePath resolves a client-supplied relative path inside a workspace after validating it (no absolute path, no '..' segments), then follows symlinks via realPathWithinRoot to confirm the real target still lands inside the workspace. This error is thrown when the candidate path does not exist (realpath fails) OR exists but its symlink-resolved real location is outside the workspace. The library throws it to prevent symlink-based escape from the workspace sandbox and to signal that the requested path cannot be served.
Source
Thrown at mastracode/factory/src/routes/fs.ts:252
): Promise<{ resolvedRoot: string; workspace: string }> {
const resolvedRoot = await realOrResolved(resolveFsRoot(root));
const candidate = isAbsolute(workspacePath) ? resolve(workspacePath) : resolve(resolvedRoot, workspacePath);
const workspace = await realPathWithinRoot(candidate, resolvedRoot);
if (!workspace) throw new Error('Path is outside the browsable root');
return { resolvedRoot, workspace };
}
async function confinedWorkspaceRelativePath(
root: string,
workspacePath: string,
relativePath: string,
): Promise<{ workspace: string; path: string; relativePath: string }> {
const safeRelativePath = assertRelativePath(relativePath, 'path');
const { workspace } = await confinedWorkspacePath(root, workspacePath);
const candidate = resolve(workspace, safeRelativePath);
if (!isWithinRoot(candidate, workspace)) throw new Error('Path escapes workspace');
const confinedPath = await realPathWithinRoot(candidate, workspace);
if (!confinedPath) throw new Error('Path is outside the workspace');
return { workspace, path: confinedPath, relativePath: safeRelativePath };
}
/**
* List the directories inside `requestedPath`, confined to `root`. An absent or
* out-of-root path is clamped to the root, so the worst a malicious client can
* do is browse within the allowed root.
*/
export async function listDirectory(root: string, requestedPath?: string): Promise<DirectoryListing> {
// Resolve the root through symlinks so all confinement checks compare real
// paths; a symlink that escapes the root is then reliably detectable.
const resolvedRoot = await realOrResolved(resolveFsRoot(root));
let target = resolvedRoot;
if (requestedPath && requestedPath.trim()) {
const candidate = isAbsolute(requestedPath) ? resolve(requestedPath) : resolve(resolvedRoot, requestedPath);
// Follow symlinks and re-confirm the real target stays within the root.
target = (await realPathWithinRoot(candidate, resolvedRoot)) ?? resolvedRoot;View on GitHub (pinned to 75dd419e61)
Solutions
- Verify the relative path exists inside the workspace directory (ls the workspace to confirm).
- Check for symlinks in the path (ls -l / readlink) and replace links that point outside the workspace with real files or copies inside it.
- Recreate the missing file if it was deleted, or refresh the client's file listing to get a valid path.
- If the intent was to browse, use the listing endpoints which clamp out-of-root paths to the workspace root instead of throwing.
Example fix
// before: path is a symlink escaping the workspace GET /fs/file?path=link/outside.txt // link -> /etc/passwd → 'Path is outside the workspace' // after: copy the target inside the workspace and reference the copy cp /etc/passwd ./data/ref.txt GET /fs/file?path=data/ref.txt
Defensive patterns
Strategy: validation
Validate before calling
import { stat, realpath } from 'node:fs/promises';
import { resolve, isAbsolute, sep } from 'node:path';
async function isSafeWorkspacePath(workspace: string, rel: string): Promise<boolean> {
const trimmed = rel.trim();
if (!trimmed || isAbsolute(trimmed) || trimmed.split(/[\\/]+/).includes('..')) return false;
const candidate = resolve(workspace, trimmed);
if (!candidate.startsWith(workspace + sep)) return false;
try {
const real = await realpath(candidate);
return real.startsWith((await realpath(workspace)) + sep);
} catch {
return false; // does not exist → would throw
}
}
// call: if (!(await isSafeWorkspacePath(ws, p))) skip the request; Type guard
function isRelativeInsideWorkspace(rel: string): boolean {
const t = rel.trim();
return t.length > 0 && !isAbsolute(t) && !t.split(/[\\/]+/).includes('..');
} Try / catch
try {
const file = await readWorkspaceFile(root, ws, path);
} catch (e) {
if (e instanceof Error && e.message === 'Path is outside the workspace') {
// treat as not-found / unsafe link: refresh listing, skip entry
} else throw e;
} Prevention
- Never pass absolute paths or '..' segments; always use relative paths returned by listing endpoints.
- Avoid symlinks in workspaces that point outside the workspace; audit with `find . -type l ! -exec realpath --relative-to . {} \;`.
- Refresh listings before reads to avoid stale paths to deleted files.
- Treat this error as 'not found or unsafe' and skip the entry rather than retrying.
When it happens
Trigger: Calling any fs route that resolves a relative path (e.g. readWorkspaceFile, listWorkspaceRenderedPath helpers) where: (1) the relative path points to a file that does not exist in the workspace, or (2) the path is (or passes through) a symlink whose real target resolves outside the workspace directory.
Common situations: A stale UI bookmark pointing at a deleted file; a symlinked node_modules or assets directory in the workspace pointing outside the browsable root; a typo in the path query param; race where a file is deleted between listing and read.
Related errors
- Path is outside the browsable root
- ${label} escapes workspace
- Path escapes workspace
- Root escapes workspace
- Path escapes workspace root: ${inputPath}
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/eff2100a2548ae17.
Report an issue: GitHub.