microsoft/playwright · error · Error

File access denied: is outside allowed roots. Allowed roots

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.

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@f1d33b5029 (2026-09-15). Data as JSON: /api/errors/fedb315175f81eb6. Report an issue: GitHub.

Appendix: source

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

  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;
  // Follow symlinks, an unresolvable root cannot be traversed anyway.
  const [realOutput, realWorkspace, realFilename] = await Promise.all([
    resolveSymlinks(output).catch(() => output),
    resolveSymlinks(workspace).catch(() => workspace),
    resolveSymlinks(resolvedFilename),
  ]);
  if (!isPathInside(realOutput, realFilename) && !isPathInside(realWorkspace, realFilename))
    throw new Error(`File access denied: ${resolvedFilename} is outside allowed roots. Allowed roots: ${output}, ${workspace}`);
}

View on GitHub (pinned to f1d33b5029)