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

  1. Pass a workspacePath that exists inside the configured root (verify with fs.realpath and check it starts with the root's realpath)
  2. Remove or replace symlinks inside the workspace that point outside the root
  3. Use the resolved root's realpath when constructing candidate paths (realOrResolved is applied to root; match it client-side)
  4. 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

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


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/fd9c1cf95f9625ec. Report an issue: GitHub.