How to fix Puppeteer screenshot EACCES permission errors in Linux
Find which path Linux denied, then fix the screenshot destination or Chrome’s runtime directories. Includes runnable Node.js, Python and cURL options.
When Puppeteer’s page.screenshot({ path }) fails with EACCES, first inspect the exact path in the error and the Linux user running Node. If the denied path is the screenshot file, make its parent directory writable by that user. If the error happens before capture, check Chrome’s profile, configuration, and cache paths instead. A relative screenshot path is resolved from Node’s current working directory, which may differ from your shell’s directory.
1. Identify which operation was denied
Read the complete error message and stack trace. Note the path named after EACCES, the failing operation (often open), and whether Chrome launched and the page loaded.
- Screenshot destination: the denied path is the output image, such as
/srv/app/shots/page.png. Check the directory, process identity, and mount permissions. - Puppeteer configuration: the denied path is a config file, for example
/.config/puppeteerrc. This is config access, not proof that the screenshot destination is unwritable. - Chrome startup: the error occurs while launching the browser or creating its profile, before the screenshot call can write the image. Check writable runtime directories and browser dependencies.
These branches need different fixes. Avoid changing permissions on unrelated directories until the denied path tells you which one is involved.
2. Fix the screenshot output path
Puppeteer’s ScreenshotOptions API documentation says a relative path is resolved relative to the current working directory. The API also allows omitting path; in that case Puppeteer returns the screenshot data instead of saving it directly to disk.
Check the runtime identity and destination
Run these checks in the same service, container, or CI job that runs Puppeteer. Running them in an interactive shell as a different user can give misleading results.
node -e 'console.log({ cwd: process.cwd(), uid: process.getuid?.(), gid: process.getgid?.() })'
# Replace this with the actual directory containing the output file.
ls -ld /srv/app/shots
# Check whether this process can create a file in that directory.
test -w /srv/app/shots && echo writable || echo not-writable
Create the directory as part of deployment or application startup, then give the runtime user ownership or write access to that specific directory. For example, an administrator can create a dedicated output directory and assign it to the service account:
sudo install -d -o appuser -g appuser -m 0750 /srv/app/shots
Replace appuser and the path with the account and location used by your service. Avoid using chmod 777 as a shortcut; grant only the access the process needs.
Runnable Node.js example
This example creates the output directory, resolves the output path explicitly, and reports the resolved path if capture fails. Install Puppeteer in your project and ensure its browser is available before running the script.
const fs = require('node:fs/promises');
const path = require('node:path');
const puppeteer = require('puppeteer');
(async () => {
const outputDir = path.resolve(process.cwd(), 'screenshots');
const outputPath = path.join(outputDir, 'page.png');
await fs.mkdir(outputDir, { recursive: true });
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: outputPath, fullPage: true });
console.log(`Saved screenshot to ${outputPath}`);
} catch (error) {
console.error(`Screenshot destination: ${outputPath}`);
throw error;
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
If the parent directory exists but capture still returns EACCES, check whether the process is running under a different UID than expected, whether the output filesystem is mounted read-only, and whether directory permissions allow traversal as well as writing. A file can also already exist with permissions that prevent replacement.
Save returned screenshot bytes yourself
If your application can handle the image buffer, omit path and choose a writable destination in your own file-writing code. This avoids Puppeteer opening the requested screenshot path, but the final write still needs filesystem permission.
const fs = require('node:fs/promises');
const path = require('node:path');
const puppeteer = require('puppeteer');
(async () => {
const outputDir = path.resolve(process.cwd(), 'screenshots');
await fs.mkdir(outputDir, { recursive: true });
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
const image = await page.screenshot({ fullPage: true, type: 'png' });
await fs.writeFile(path.join(outputDir, 'page.png'), image);
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
3. Fix Chrome profile, config, and cache access
Chrome writes profile, configuration, and cache data during startup. In a read-only container or a container with restricted mounts, those locations may not be writable even when the screenshot output directory is. Puppeteer’s troubleshooting guide describes setting writable XDG config and cache locations and an explicit userDataDir, or mounting writable directories owned by the runtime user.
For a container, configure these locations to directories that the Node process can write. Ensure the directories exist and their ownership matches the runtime UID/GID. The following is a launch pattern; adapt the paths to writable locations provided by your deployment:
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({
userDataDir: '/tmp/puppeteer-profile',
env: {
...process.env,
XDG_CONFIG_HOME: '/tmp/xdg-config',
XDG_CACHE_HOME: '/tmp/xdg-cache',
},
});
If those paths are on a mounted volume, verify the mount is writable and owned or permissioned for the process account. Do not assume that a path writable on your workstation is writable inside the deployed container.
4. Check other Linux browser launch failures separately
If Chrome does not launch, a permission error is only one possible cause. Puppeteer’s troubleshooting guidance recommends checking missing shared libraries with ldd. Locate the Chrome executable used by your installation, then inspect its dependencies:
ldd /path/to/chrome | grep 'not found'
Install the missing system libraries using the package manager and base image appropriate to your Linux distribution. The command’s output identifies missing dependencies; it does not diagnose an unwritable screenshot directory.
Sandbox errors are also a separate launch issue. Puppeteer strongly discourages running Chrome without its sandbox as a casual workaround. Diagnose the container’s sandbox setup using the official troubleshooting guidance instead of adding --no-sandbox to hide an unrelated permission problem.
5. Troubleshooting by symptom
| Symptom | Likely cause | What to check or change |
|---|---|---|
EACCES names the PNG/JPEG/WebP output path |
Node cannot create or replace the file, or cannot traverse/write its parent directory | Resolve the path; check directory existence, process UID/GID, ownership, mode, and mount options. Use a directory writable by the service user. |
| A relative path works locally but fails in CI or a service | The process current working directory differs, or that directory is not writable | Log process.cwd(); resolve the destination explicitly and use a dedicated writable directory. |
The named path is /.config/puppeteerrc or another config file |
Puppeteer is trying to read configuration from an inaccessible location | Inspect the runtime home/config environment and file ownership. Do not treat this as evidence that the image output path is the problem. |
Chrome fails before page.screenshot() |
Profile, config, or cache directory is unwritable; browser dependencies may also be missing | Set writable XDG paths and userDataDir, or fix mounted directory ownership. Check dependencies with ldd. |
| Output directory exists, but writes still fail in a container | The mounted filesystem may be read-only or mapped to a different host identity | Inspect the container volume and mount mode; provide a writable mount with permissions for the runtime UID/GID. |
| The file exists but cannot be overwritten | The existing file’s owner or mode prevents replacement | Choose a fresh output name or correct ownership and permissions for that output file. |
Adding --no-sandbox appears to change the failure |
This changes browser isolation behavior; it does not fix a screenshot path permission | Recheck the failing stage and use Puppeteer’s sandbox troubleshooting guidance. Do not apply it as a generic EACCES fix. |
6. Reliability, performance, and cost considerations
Permission fixes should be part of the runtime setup, not an assumption based on a developer’s local account. Create output and browser-runtime directories during image build or deployment, run the process with a stable non-root identity, and ensure mounted paths use compatible ownership and write permissions. Keep screenshot outputs in a dedicated directory so its permissions can be managed independently.
Returning image bytes instead of asking Puppeteer to save directly changes where the write happens; it does not remove the need for writable storage if you later save the image. Browser startup and page loading are separate from file writing, so diagnose them as separate stages. This permission issue itself provides no basis for a general speed or failure-rate estimate.
Or skip the browser setup
If you need screenshots without managing a Linux browser, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF. The API accepts parameter names used by other screenshot APIs, which can make switching straightforward. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
- Cookie banners are accepted and 60+ known consent platforms, newsletter popups, and chat widgets are removed before the shot; each step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. Responses identify the page verdict and billing status in headers.
- An MCP server offers
take_screenshot,get_page_info, andcapture_pdftools for AI agents. - The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is on every plan.
Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Does EACCES always mean the screenshot folder is wrong?
No. The path in the error may belong to Puppeteer configuration or Chrome’s runtime files. Use the denied path and stack trace to identify the operation.
Does changing to an absolute path fix permissions?
It removes ambiguity about the working directory, but the process still needs access to the destination and its parent directories.
Can I avoid writing screenshots to disk?
Yes. Omit Puppeteer’s path option and handle the returned image data in memory or pass it to a storage layer. Any later filesystem write still requires permission.


