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

  1. Ensure the sandbox environment provides a working `realpath` binary reachable by the exec layer
  2. Run a simple smoke exec (e.g. `realpath /`) through the same sandbox exec to confirm clean stdout with no wrapper noise
  3. Re-run the operation — transient exec/shell failures can trip this check
  4. 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

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


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