mastra-ai/mastra · error

${label} must be relative

Error message

${label} must be relative

What it means

`assertRelativePath` only accepts workspace-relative paths and throws `${label} must be relative` when the supplied query param parses as an absolute path (detected via `isAbsolute`, e.g. starts with '/' on POSIX or 'C:\' on Windows). This prevents clients from reading or writing outside the workspace root by supplying fully qualified filesystem paths.

Source

Thrown at mastracode/factory/src/routes/fs.ts:217

/**
 * Resolve a path's real location (following symlinks) and confirm it stays
 * within `root`. Returns the real path when confined, or `null` when it escapes
 * the root or does not exist. Used so a symlink inside the root that points
 * outside it cannot be browsed or selected.
 */
async function realPathWithinRoot(candidate: string, root: string): Promise<string | null> {
  try {
    const real = await realpath(candidate);
    return isWithinRoot(real, root) ? real : null;
  } catch {
    return null;
  }
}

function assertRelativePath(path: string, label: string): string {
  const trimmed = path.trim();
  if (!trimmed) throw new Error(`Missing required query param: ${label}`);
  if (isAbsolute(trimmed)) throw new Error(`${label} must be relative`);
  if (trimmed.split(/[\\/]+/).includes('..')) throw new Error(`${label} escapes workspace`);
  const normalized = resolve('/', trimmed).slice(1);
  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));

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Send the path relative to the workspace root (e.g. `src/index.ts`, not `/work/src/index.ts`).
  2. On the client, strip the workspace root prefix before sending: `path.relative(workspaceRoot, absPath)`.
  3. Never pass results of `path.resolve()`/`path.join(absoluteRoot, ...)` directly as the query param.
  4. Normalize Windows-style paths to forward-slash relative form before the request.

Example fix

// before
const p = path.join(WORKSPACE_ROOT, 'src/app.ts');
fetch(`/api/fs/read?path=${encodeURIComponent(p)}`); // absolute => throws
// after
const p = path.relative(WORKSPACE_ROOT, path.join(WORKSPACE_ROOT, 'src/app.ts'));
fetch(`/api/fs/read?path=${encodeURIComponent(p)}`); // 'src/app.ts'
Defensive patterns

Strategy: validation

Validate before calling

function toWorkspaceRelative(workspaceRoot: string, p: string): string {
  const rel = path.relative(workspaceRoot, p);
  if (!rel || rel.startsWith('..') || path.isAbsolute(p)) {
    throw new Error(`path must be workspace-relative, got: ${p}`);
  }
  return rel.split(path.sep).join('/');
}

Try / catch

try {
  return await api.fsRead({ path });
} catch (err) {
  if (err instanceof Error && err.message.endsWith('must be relative')) {
    return api.fsRead({ path: toWorkspaceRelative(workspaceRoot, path) });
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling an fs route with a param like `?path=/etc/passwd`, `?path=C:\Users\...`, or any value produced by `path.resolve()`/`absolute()` on the client side instead of a path relative to the workspace.

Common situations: Client joining an absolute base directory with the filename (`path.join(workspaceRoot, file)`) and sending the result; paths copied from absolute file watchers; cross-platform bugs where Windows drive-letter paths appear after syncing from a Windows machine.

Related errors


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