mastra-ai/mastra · error
Path is outside the browsable root
Error message
Path is outside the browsable root
What it means
confinedWorkspacePath resolves the configured workspace root, resolves the requested workspacePath (absolute or relative to that root), then follows symlinks via realPathWithinRoot to confirm the real location stays inside the root. It throws "Path is outside the browsable root" when the candidate path does not exist within the root or resolves (through symlinks) to a location outside it — an anti-traversal/anti-symlink-escape guard.
Source
Thrown at mastracode/factory/src/routes/fs.ts:238
if (!normalized || normalized === '..' || normalized.startsWith(`..${sep}`))
throw new Error(`${label} escapes workspace`);
return normalized;
}
function assertApprovedRenderedRoot(renderedRoot: string): string {
const safeRoot = assertRelativePath(renderedRoot, 'root');
if (!APPROVED_RENDERED_ROOTS.has(safeRoot)) throw new Error('Root is not approved for rendered workspace access');
return safeRoot;
}
async function confinedWorkspacePath(
root: string,
workspacePath: string,
): 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 };
}
/**View on GitHub (pinned to 75dd419e61)
Solutions
- Pass a workspacePath that exists inside the configured root (verify with fs.realpath and check it starts with the root's realpath)
- Remove or replace symlinks inside the workspace that point outside the root
- Use the resolved root's realpath when constructing candidate paths (realOrResolved is applied to root; match it client-side)
- If the target must be browsable, place it physically (or symlink-safe) inside the approved root
Example fix
// before
const res = await fetch(`/api/fs/list?workspacePath=${encodeURIComponent(linkToOutside)}`);
// after
const real = await fs.realpath(target);
const rootReal = await fs.realpath(workspaceRoot);
if (!real.startsWith(rootReal + path.sep)) throw new Error('outside browsable root');
const res = await fetch(`/api/fs/list?workspacePath=${encodeURIComponent(path.relative(rootReal, real))}`); Defensive patterns
Strategy: validation
Validate before calling
import { realpath, realpathSync } from 'node:fs/promises';
async function assertInsideRoot(p: string, root: string): Promise<string> {
const [realP, realRoot] = await Promise.all([realpath(p).catch(() => null), realpath(root)]);
if (!realP || !(realP === realRoot || realP.startsWith(realRoot + require('path').sep))) {
throw new Error('path is outside the browsable root');
}
return realP;
} Type guard
function isWithinRoot(candidate: string, root: string): boolean {
const rel = require('path').relative(root, candidate);
return rel === '' || (!rel.startsWith('..') && !require('path').isAbsolute(rel));
} Try / catch
try {
const listing = await browseRoute({ workspacePath });
} catch (err) {
if (err instanceof Error && err.message === 'Path is outside the browsable root') {
workspacePath = '.'; // clamp to the configured root
return await browseRoute({ workspacePath });
}
throw err;
} Prevention
- realpath both the target and the root before comparing (symlink-aware check)
- Avoid symlinks inside workspaces that point outside the root
- Verify the target exists before calling — missing paths also yield this error
- Use paths derived from the root's realpath, since the server resolves the root first
When it happens
Trigger: Calling routes backed by the { workspace } handler with a workspacePath that points outside resolveFsRoot(root); a symlink inside the workspace pointing to e.g. /etc; a nonexistent path (realpath fails, returns null); or a root whose realpath differs from the configured root so candidate resolves under the non-real path.
Common situations: UI passing a bookmarked absolute path from a previous machine or different workspace; a symlinked directory (e.g. node_modules -> shared cache) inside the workspace; stale links after the workspace was moved or deleted; mounting the workspace via a symlinked path so resolvedRoot and candidate diverge.
Related errors
- Path is outside the workspace
- ${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/fd9c1cf95f9625ec.
Report an issue: GitHub.