mastra-ai/mastra · error
Root escapes workspace
Error message
Root escapes workspace
What it means
listWorkspaceRenderedPath resolves an approved rendered root (e.g. '.artifacts') against the workspace directory and then checks the computed absolute path stays within the workspace. 'Root escapes workspace' is thrown when resolving the approved root name against the workspace produces a path outside the workspace — practically only possible if the approved-root allowlist or the workspace resolution changed, since safeRoot has already been validated as relative and '..'-free. It is a defense-in-depth containment check before any filesystem access.
Source
Thrown at mastracode/factory/src/routes/fs.ts:345
type: 'file',
size: info.size,
updatedAt: info.mtime.toISOString(),
});
}
}
return entries.sort((a, b) => a.path.localeCompare(b.path));
}
export async function listWorkspaceRenderedPath(
root: string,
workspacePath: string,
renderedRoot: string,
): Promise<WorkspaceRenderedListing> {
const safeRoot = assertApprovedRenderedRoot(renderedRoot);
const { workspace } = await confinedWorkspacePath(root, workspacePath);
const renderedPath = resolve(workspace, safeRoot);
if (!isWithinRoot(renderedPath, workspace)) throw new Error('Root escapes workspace');
const confinedRootPath = await realPathWithinRoot(renderedPath, workspace);
if (!confinedRootPath) return { workspacePath: workspace, root: safeRoot, rootPath: renderedPath, entries: [] };
const info = await stat(confinedRootPath);
if (!info.isDirectory()) return { workspacePath: workspace, root: safeRoot, rootPath: confinedRootPath, entries: [] };
return {
workspacePath: workspace,
root: safeRoot,
rootPath: confinedRootPath,
entries: await listRenderedEntries(confinedRootPath),
};
}
export async function readWorkspaceFile(root: string, workspacePath: string, path: string): Promise<WorkspaceFile> {
const safePath = assertRelativePath(path, 'path');
const relativeRoot = safePath.split('/')[0] ?? '';View on GitHub (pinned to 75dd419e61)
Solutions
- Check the renderedRoot argument — it must be one of the approved roots (e.g. '.artifacts') and contain no '..' or absolute components.
- Inspect APPROVED_RENDERED_ROOTS and remove/fix any entry that could resolve outside the workspace.
- Confirm the workspace itself resolved correctly (confinedWorkspacePath) — an empty or odd workspacePath can make resolve() land somewhere unexpected.
- Ensure .artifacts (or the relevant root) is a real directory inside the workspace, not a symlink out of it.
Example fix
// before APPROVED_RENDERED_ROOTS = new Set(['.artifacts', '../shared-out']) // after APPROVED_RENDERED_ROOTS = new Set(['.artifacts'])
Defensive patterns
Strategy: validation
Validate before calling
import { resolve } from 'node:path';
const APPROVED = new Set(['.artifacts']);
function isApprovedRoot(root: string): boolean {
const t = root.trim();
return APPROVED.has(t) && !t.split(/[\\/]+/).includes('..') && resolve('/workspace', t).startsWith('/workspace');
}
if (!isApprovedRoot(renderedRoot)) throw new Error('refusing request: root not approved'); Type guard
function isApprovedRenderedRoot(root: string): root is '.artifacts' {
return root === '.artifacts';
} Try / catch
try {
const listing = await listWorkspaceRenderedPath(root, ws, renderedRoot);
} catch (e) {
if (e instanceof Error && e.message === 'Root escapes workspace') {
// containment violation: log and return empty listing
return { workspacePath: ws, root: renderedRoot, entries: [] };
}
throw e;
} Prevention
- Only pass literal approved root names (e.g. '.artifacts'), never user-supplied or concatenated strings.
- Keep the APPROVED_RENDERED_ROOTS allowlist free of traversal or absolute entries; add a unit test asserting each entry resolves inside the workspace.
- Ensure the rendered root is a real directory inside the workspace, not an outbound symlink.
- Don't modify confinement helpers (resolve/isWithinRoot) without re-running the containment tests.
When it happens
Trigger: Calling listWorkspaceRenderedPath with a renderedRoot that resolves outside the workspace. With the current allowlist this is nearly unreachable from user input (assertApprovedRenderedRoot rejects non-approved roots and any '..'), so hits usually mean a modified APPROVED_RENDERED_ROOTS entry, a mutated resolve/isWithinRoot helper, or a workspace resolution inconsistency.
Common situations: A developer adding a new entry to APPROVED_RENDERED_ROOTS that contains a traversal or absolute component; a symlink at workspace/.artifacts pointing outside the workspace combined with a helper bypass; custom forks changing the confinement helpers.
Related errors
- ${label} escapes workspace
- Path is outside the browsable root
- Path escapes workspace
- Path is outside the workspace
- Path escapes workspace root: ${inputPath}
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/5ed6c4cb1d0d1d6f.
Report an issue: GitHub.