mastra-ai/mastra · error
Unable to verify path stays within workspace root: ${inputPa
Error message
Unable to verify path stays within workspace root: ${inputPath} What it means
Before reading, deleting, copying, moving, or removing directories, the sandbox canonicalizes the path (and the workspace root) with a shell `realpath` to detect symlink escapes. If the command fails or returns unparsable output (missing root/real lines), the library cannot prove containment, so it throws rather than silently allowing an unverifiable path. It is a fail-closed safety check.
Source
Thrown at mastracode/sdk/src/agents/sandbox-filesystem.ts:212
const result = await this.exec(
[
`p=${shellQuote(abs)}`,
`if [ ! -e "$p" ] && [ ! -L "$p" ]; then exit ${EXIT_NOT_FOUND}; fi`,
// The workdir itself may contain symlinked components (/tmp on macOS),
// so canonicalize it as the comparison root.
`root=$(cd ${shellQuote(this.basePath)} 2>/dev/null && pwd -P)`,
`[ -n "$root" ] || exit 1`,
`rp=$(realpath "$p" 2>/dev/null) || rp=$(readlink -f "$p" 2>/dev/null) || { [ -d "$p" ] && rp=$(cd "$p" 2>/dev/null && pwd -P); }`,
`[ -n "$rp" ] || exit 1`,
`printf '%s\\n%s' "$root" "$rp"`,
].join('\n'),
);
// Path doesn't exist yet: nothing to canonicalize (writes to a fresh leaf
// are covered by assertContainedDest checking the parent directory).
if (result.exitCode === EXIT_NOT_FOUND) return;
const [root, real] = result.stdout.split('\n').map(s => s.trim());
if (result.exitCode !== 0 || !root || !real) {
throw new Error(`Unable to verify path stays within workspace root: ${inputPath}`);
}
if (real !== root && !real.startsWith(`${root}/`)) {
throw new Error(`Path escapes workspace root (symlink): ${inputPath}`);
}
}
/**
* Guard for write destinations. The lexical guard catches `..`, but a symlink
* inside the workdir can redirect a write outside it. For an existing target
* we check its realpath; for a not-yet-existing target we check the realpath
* of its nearest existing ancestor directory, since a symlinked parent is the
* escape vector (e.g. `link -> /etc` then writing `link/passwd`).
*/
private async assertContainedDest(abs: string, inputPath: string): Promise<void> {
// First check the target itself (covers overwriting an existing symlink).
await this.assertContainedRealpath(abs, inputPath);
// Then check the parent directory's realpath; readlink -f resolves the
// nearest existing ancestor when the leaf doesn't exist yet.View on GitHub (pinned to 75dd419e61)
Solutions
- Ensure the sandbox environment provides a working `realpath` binary reachable by the exec layer
- Run a simple smoke exec (e.g. `realpath /`) through the same sandbox exec to confirm clean stdout with no wrapper noise
- Re-run the operation — transient exec/shell failures can trip this check
- If the environment cannot offer realpath, relax or bypass the symlink check consciously and rely solely on the lexical containment guard
Example fix
// before (in a stripped container)
await fs.readFile('data/file.txt'); // throws: unable to verify
// after: install coreutils or verify exec works
// docker run ... coreutils (provides realpath), then
await fs.readFile('data/file.txt'); Defensive patterns
Strategy: try-catch
Validate before calling
// Probe that the sandbox exec layer returns clean output and realpath exists
const probe = await sandbox.exec('realpath /');
if (probe.exitCode !== 0 || probe.stdout.trim() === '') {
throw new Error('Sandbox cannot canonicalize paths: realpath unavailable or stdout polluted');
} Type guard
function isContainmentVerified(res: { exitCode: number; stdout: string }): boolean {
if (res.exitCode === 0) {
const [root, real] = res.stdout.split('\n').map(s => s.trim());
return Boolean(root && real);
}
return false;
} Try / catch
try {
const content = await fs.readFile(p);
} catch (err) {
if (err instanceof Error && err.message.startsWith('Unable to verify path stays within workspace root')) {
// fail closed: do not fall back to an unverified read
// surface an environment problem (missing realpath / noisy shell)
}
throw err;
} Prevention
- Use sandbox images that include coreutils (realpath) and keep exec output clean of wrappers/banners
- Fail closed on verification failures — never bypass containment checks to 'make it work'
- Smoke-test the sandbox exec layer after environment or image changes
- Treat injected shell profile output (MOTD, echo in rc files) as a bug and remove it
When it happens
Trigger: assertContainedRealpath is invoked (readFile, deleteFile, copyFile, moveFile, rmdir, assertContainedDest) on an existing path whose `realpath` invocation returns a non-zero exit other than the handled not-found code, or whose stdout does not contain both the real workspace root and the resolved path (e.g. `\n`-split lines missing/empty).
Common situations: Restricted sandbox/container environments where `realpath` (coreutils) is unavailable or blocked; a shell wrapper that injects extra output (banners, MOTD, proxies) corrupting stdout; sandbox exec layer returning stderr content on stdout; exotic filesystems or corrupted mounts making stat calls fail.
Related errors
- ${context} failed (exit ${result.exitCode}): ${result.stderr
- readFile ${path} failed (exit ${result.exitCode}): ${result.
- writeFile ${path} failed (exit ${result.exitCode}): ${result
- Directory not empty or not found: ${path}
- Invalid environment variable name: "${key}"
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/93396d4c7fbdbdc0.
Report an issue: GitHub.