From 4bb1b261371e1da1471f3c12eb47410c0ed740c5 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sat, 25 Apr 2026 21:39:13 -0600 Subject: [PATCH] feat: strip macOS Gatekeeper quarantine after browser install macOS sets com.apple.quarantine on network-downloaded files. On Tahoe (macOS 26) and later, Gatekeeper enforcement silently blocks launch of quarantined Chromium binaries, causing confusing "browser failed to start" errors after a successful install. After `playwright install` completes on darwin, run `xattr -dr com.apple.quarantine` against the browser cache directory (~/Library/Caches/ms-playwright by default, or PLAYWRIGHT_BROWSERS_PATH when set). Best-effort: errors are logged via testDebug and never thrown. Skipped on Linux/Windows and when PLAYWRIGHT_BROWSERS_PATH=0 (node_modules install path doesn't get quarantined). --- src/browserInstaller.ts | 43 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/browserInstaller.ts b/src/browserInstaller.ts index e69abff..354da39 100644 --- a/src/browserInstaller.ts +++ b/src/browserInstaller.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { fork } from 'node:child_process'; +import { fork, spawn } from 'node:child_process'; +import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -62,6 +63,12 @@ async function runInstall(target: string): Promise { await runPlaywrightCli(cliPath, ['install', target]); + // macOS: strip Gatekeeper quarantine attribute from freshly downloaded + // browser binaries. Without this, the first launch is silently blocked + // by macOS on a fresh install. + if (process.platform === 'darwin') + await stripDarwinQuarantine(); + // Best-effort system-deps install. Only runs when we're already root, // otherwise skipped silently — users will see Playwright's own missing-lib // error on the next launch, which tells them exactly what to apt install. @@ -74,6 +81,40 @@ async function runInstall(target: string): Promise { } } +/** + * Removes the `com.apple.quarantine` extended attribute from Playwright's + * browser cache directory. macOS sets this on anything downloaded from the + * network; on Tahoe and later it causes Gatekeeper to silently block launch. + * + * Best-effort: errors are logged but never thrown. If `xattr` doesn't exist, + * the cache dir is missing, or the attribute isn't present, we just move on. + */ +async function stripDarwinQuarantine(): Promise { + const cacheDir = process.env.PLAYWRIGHT_BROWSERS_PATH + || path.join(os.homedir(), 'Library', 'Caches', 'ms-playwright'); + + // PLAYWRIGHT_BROWSERS_PATH=0 means "install into node_modules" — skip, + // since those binaries weren't downloaded with quarantine in that flow. + if (cacheDir === '0') + return; + + testDebug(`stripping quarantine attribute from ${cacheDir}`); + await new Promise(resolve => { + const child = spawn('/usr/bin/xattr', ['-dr', 'com.apple.quarantine', cacheDir], { + stdio: 'pipe', + }); + child.on('close', code => { + if (code !== 0) + testDebug(`xattr exited ${code} (non-fatal)`); + resolve(); + }); + child.on('error', err => { + testDebug(`xattr spawn failed (non-fatal): ${err.message}`); + resolve(); + }); + }); +} + function resolvePlaywrightCli(): string { try { const cliUrl = import.meta.resolve('playwright/package.json');