microsoft/playwright · error · Error

File access denied: ${resolvedFilename} is outside allowed r

Error message

File access denied: ${resolvedFilename} is outside allowed roots. Allowed roots: ${output}, ${workspace}

What it means

Thrown by checkFile (used by workspaceFile/outputFile) when an LLM-origin (flags.origin === 'llm') file path resolves outside both the output directory and the workspace (options.cwd). Code-origin calls, allowUnrestrictedFileAccess, and skillMode all bypass this sandbox check.

Source

Thrown at packages/playwright-core/src/tools/backend/context.ts:423

export async function outputFile(options: ContextOptions, fileName: string, flags: { origin: 'code' | 'llm' }): Promise<string> {
  const resolvedFile = path.resolve(outputDir(options), fileName);
  await checkFile(options, resolvedFile, flags);
  await fs.promises.mkdir(path.dirname(resolvedFile), { recursive: true });
  debug('pw:mcp:file')(resolvedFile);
  return resolvedFile;
}

async function checkFile(options: ContextOptions, resolvedFilename: string, flags: { origin: 'code' | 'llm' }) {
  // Trust code and unrestricted file access.
  if (flags.origin === 'code' || options.config.allowUnrestrictedFileAccess || options.config.skillMode)
    return;

  // Trust llm to use valid characters in file names.
  const output = outputDir(options);
  const workspace = options.cwd;
  if (!isPathInside(output, resolvedFilename) && !isPathInside(workspace, resolvedFilename))
    throw new Error(`File access denied: ${resolvedFilename} is outside allowed roots. Allowed roots: ${output}, ${workspace}`);
}

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Use a workspace-relative path (relative to options.cwd) for the file.
  2. If writing tool artifacts, route through outputFile() which already anchors to outputDir.
  3. For trusted automation, enable allowUnrestrictedFileAccess or skillMode in the config to bypass the sandbox.
  4. Verify the resolved absolute path with path.resolve and confirm it lives under cwd before invoking the tool.

Example fix

// before (agent supplies absolute path outside roots)
await workspaceFile(options, '/etc/secrets.txt'); // throws

// after
await workspaceFile(options, 'artifacts/secrets.txt'); // resolves under options.cwd
Defensive patterns

Strategy: validation

Validate before calling

import path from 'path';

function isInside(root: string, candidate: string): boolean {
  const rel = path.relative(root, candidate);
  return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
}

function assertFileAllowed(opts: { cwd: string; outputDir?: string }, absPath: string) {
  const output = opts.outputDir ?? /* mirror outputDir() logic */ path.join(opts.cwd, '.playwright-mcp');
  if (!isInside(output, absPath) && !isInside(opts.cwd, absPath))
    throw new Error(`Refusing path outside roots: ${absPath}`);
}

Type guard

function isPathInsideRoot(root: string, candidate: string): boolean {
  const rel = path.relative(root, candidate);
  return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
}

Try / catch

try {
  await context.workspaceFile(name, perCallDir);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('File access denied:')) {
    // rewrite to a workspace-relative path and retry
  } else throw e;
}

Prevention

When it happens

Trigger: An LLM-driven MCP tool resolving a file path (read, write, screenshot output, PDF output, upload) that resolves via path.resolve to a location outside outputDir(options) AND outside options.cwd.

Common situations: Agent passes an absolute path like /etc/passwd or /tmp/secret; relative path with ../ that escapes the workspace; outputDir falling back to a tmp dir while the agent assumed the workspace dir.

Understand the failure class

Related errors


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/fedb315175f81eb6. Report an issue: GitHub.