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

  1. Verify the relative path exists inside the workspace directory (ls the workspace to confirm).
  2. Check for symlinks in the path (ls -l / readlink) and replace links that point outside the workspace with real files or copies inside it.
  3. Recreate the missing file if it was deleted, or refresh the client's file listing to get a valid path.
  4. 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

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


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