How to Fix Puppeteer Screenshot Permission Errors on macOS
A Puppeteer page screenshot and a native Mac screen capture use different paths. Identify which one is failing, then follow the fix that matches its API and error.
A Puppeteer screenshot permission error on macOS does not automatically mean that Puppeteer needs Screen Recording access. First identify the capture API: Puppeteer’s Page.screenshot() saves a rendered browser page, while Apple’s ScreenCaptureKit captures native screen content such as the desktop or an app window. Apply macOS Screen Recording permissions only when the failing code actually uses that native capture path.
For an ordinary webpage screenshot, start with Puppeteer’s documented page API and investigate the exact failing call, browser launch mode, and output path if it errors. Puppeteer’s screenshot guide shows saving the page image with Page.screenshot().
1. Identify which screenshot path is failing
Trace the call that produces the error. A Puppeteer page screenshot and native screen capture may both be described as “screenshots,” but they capture different things and have different permission requirements.
| Capture path | What it captures | Where to investigate |
|---|---|---|
page.screenshot() |
The rendered page in a browser tab | Puppeteer call, browser launch, page load, and file output |
| ScreenCaptureKit or another native capture API | Mac screen content, such as a display or window | The capturing app’s macOS Screen Recording permission and native API error |
Wrappers, test harnesses, extensions, or custom modules can call native APIs even if the surrounding project uses Puppeteer. Check the stack trace and dependencies. If a permission dialog identifies an app you did not expect, verify which process owns the actual capture call before changing settings.
2. Reproduce a normal Puppeteer page screenshot
Run a minimal script from the same project and terminal or IDE that produced the error. This establishes whether the issue occurs in the basic page screenshot path or only in a wrapper or test setup.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'screenshot.png' });
console.log('Saved screenshot.png');
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Install the full puppeteer package if needed with npm install puppeteer. This CommonJS example can run as node screenshot.js. The screenshot call writes the image to the path you supply.
If this minimal example succeeds, compare it with the original code: the capture library, browser executable, launch options, test runner, page state, and destination path. If it fails, record the exact error and the environment details listed below.
3. Record the browser mode and executable
Puppeteer launches headless by default. Setting headless: false opens a visible browser. The separate headless: 'shell' option uses the old headless shell. These modes are useful diagnostic variables; changing modes is not a universal permission fix. See the Puppeteer headless modes guide.
const browser = await puppeteer.launch({ headless: false });
// For the separate old headless shell mode:
// const browser = await puppeteer.launch({ headless: 'shell' });
Reproduce with the same URL and screenshot code in the default mode, then compare only if necessary with the mode used by the failing application. Note whether it fails only in headful mode. Also record:
- Exact error text and the stack frame where it originates.
- Puppeteer package version and macOS version.
- Browser executable, channel, and version, including whether a custom executable is configured.
- Launch mode: default headless, headful, or
'shell'. - The application named in any macOS permission prompt.
- Whether the failure is in a minimal script or only in a test runner, wrapper, or production process.
Those details help distinguish a browser or file-output failure from a native screen-capture denial. The current Puppeteer documentation describes these modes; it does not establish that every browser combination behaves identically on every macOS release.
4. If the code uses native screen capture, grant the relevant permission
This branch applies when the application actually captures desktop, display, or window content through ScreenCaptureKit. Apple instructs apps to request Screen Recording permission before capture and to include NSScreenCaptureUsageDescription to explain why the app needs access. See Apple’s ScreenCaptureKit documentation.
- Identify the app that owns the ScreenCaptureKit call. Use the process named in the error or permission prompt, not an assumed app such as Terminal, Chrome, or Node.
- Review that app’s permission under macOS System Settings > Privacy & Security > Screen Recording. The exact settings presentation can vary by macOS version.
- Grant access if appropriate for the app and capture you intend to run.
- Quit and restart the capturing app after granting permission. Apple’s macOS ScreenCaptureKit sample says a restart is needed after permission is granted for capture to work.
Apple’s sample targets macOS 15 or later and Xcode 16 or later. Check version-specific Apple documentation if the app or operating system differs. Do not apply these steps to an ordinary Page.screenshot() error unless evidence connects it to native screen capture.
5. Use the error owner to choose the fix
SCStreamErrorUserDeclined is Apple’s ScreenCaptureKit error for a user not granting Screen Recording permission to the app. That points to the native capture permission path; it is not documented in the reviewed Puppeteer page screenshot references as an error from Page.screenshot(). Check which API emitted it and follow the native permission steps above.
If the trace ends in Puppeteer or Chrome and does not involve ScreenCaptureKit, do not infer that Screen Recording permission is the cause. Investigate the failing Puppeteer call, page load, launch configuration, and file destination instead.
6. Troubleshoot common failures
| Symptom | Likely area to inspect | Next step |
|---|---|---|
| A dialog asks for Screen Recording access | Native screen capture may be involved; the prompt alone does not prove the Puppeteer page API needs it. | Trace the capture call and identify the app named by macOS. Grant access only if that app is intentionally capturing screen content. |
SCStreamErrorUserDeclined |
ScreenCaptureKit reports that permission was declined. | Review Screen Recording permission for the actual native capturing app, then restart it after granting access. |
page.screenshot() rejects without a macOS permission prompt |
Browser/page failure or screenshot options may be responsible. | Use the minimal example, capture the complete error and stack, and compare launch mode and browser executable. |
| Screenshot call succeeds but no file appears | Destination path, current working directory, or filesystem access. | Use an explicit writable path, check the process working directory, and inspect the returned error rather than changing Screen Recording access. |
| Minimal script works but test suite fails | Test runner context, parallel browser lifecycle, wrapper, or different executable/options. | Compare the exact launch and capture configuration and run the failing test alone. |
| Failure occurs only with visible Chrome | Headful-specific environment or launch behavior. | Record the mode and executable and reproduce with the same setup. A mode difference is diagnostic, not proof of a permission issue. |
Before resetting privacy permissions, establish that native capture is involved and identify the relevant app. A broad reset changes macOS privacy authorization state, and the cited Puppeteer documentation does not recommend it as a remedy for Page.screenshot().
7. Keep capture reliable and efficient
- Use a minimal reproduction: one browser, one page, one navigation, one screenshot. Add wrappers and test-runner behavior back only after the basic path is clear.
- Close the browser in a
finallyblock: this prevents a failed navigation or capture from leaving the launched browser process behind. - Separate navigation from capture: log or handle failures at
goto()andscreenshot()independently so a page-load timeout is not mistaken for a permission denial. - Use an explicit output path: make it clear where the image is written and ensure the process can write there.
- Keep browser identity consistent: a custom executable or a different launch mode can change the environment being diagnosed. Record it when comparing runs.
There is no universal performance or cost figure for this local workflow in the cited documentation. Capture time depends on navigation, page content, browser startup, and screenshot settings. For reliability, preserve the exact error and environment details with each reproduction rather than treating every failed image as a privacy problem.
Or skip the browser setup
If your goal is a clean webpage image rather than capturing the Mac’s desktop, ScreenshotNeo offers a website screenshot API. See the API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js:
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}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
- Cookie banners are accepted and removed before capture, along with supported newsletter popups and chat widgets.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers report the page verdict and billing status.
- An MCP server provides
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.
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
FAQ
Does Puppeteer need Screen Recording permission on Mac?
The documented Page.screenshot() API captures a browser page. Screen Recording permission is relevant when the failing code captures native screen content, such as through ScreenCaptureKit.
Should I enable Screen Recording for Terminal or Chrome?
Only grant permission to the app that actually performs native screen capture, after confirming its identity and purpose. Do not assume Terminal or Chrome needs it for a page screenshot.
Will switching to headless mode fix the error?
Not universally. Puppeteer defaults to headless mode, and headful and shell modes are distinct launch choices. Compare modes to gather diagnostic evidence, then fix the cause shown by the error.
What does SCStreamErrorUserDeclined mean?
Apple defines it as the user not granting Screen Recording permission to the app using ScreenCaptureKit. Verify that the failing path is native capture before applying that diagnosis to a Puppeteer project.
Can I use this method to capture the whole Mac desktop?
No. Page.screenshot() captures the browser page. Native desktop or window capture requires an appropriate native capture API and its permission flow.


