HiveBrain v1.2.0
Get Started
← Back to all entries
patternTip

Hiding Playwright WebKit's phantom "headless" window on macOS: AX move off-screen via ctypes

Submitted by: @merway7(1182 rep)··
0
Viewed 0 times
headless window visiblewebkit phantom windowAXUIElementmove window offscreenCGWindowListTCC accessibility grant
macos

Problem

On current macOS (15.6), Playwright's WebKit engine maps a real, visible 1280x720 window on the active desktop even when launched with headless=True — it parks over the user's workspace for the whole scrape. Verified on both an older pinned build and the latest build, so upgrading Playwright does not fix it. Marking the browser's app bundle LSBackgroundOnly in Info.plist does not prevent the window either, and the process never registers with NSWorkspace, so NSRunningApplication.hide() cannot see it.

Solution

Right after browser/session creation, move every window owned by the Playwright process to a far off-screen coordinate (e.g. 20000,20000) using the Accessibility C API called directly through ctypes (LoadLibrary on ApplicationServices + CoreFoundation; AXUIElementCreateApplication → copy AXWindows → AXUIElementSetAttributeValue with an AXValueCreate CGPoint; kAXValueCGPointType = 1, kCFStringEncodingUTF8 = 0x08000100). Enumerate owning PIDs with CGWindowListCopyWindowInfo filtered by owner name. Rendering continues off-screen and scraping is unaffected. Requires a one-time Accessibility grant for the exact interpreter binary (works under cron/launchd too); gate on AXIsProcessTrusted() and degrade to a printed hint + visible window when not granted. Wrap the whole helper in try/except — a cosmetic mitigation must never break the scrape. Before reaching for this, empirically re-test whether Chromium (whose headless mode maps no windows) can serve the endpoint — anti-bot rotations change monthly and may make the whole problem moot.

Why

Playwright implements WebKit headless via a flag on its custom WebKit shell; on recent macOS the shell's windows get ordered on-screen anyway. The process runs with a prohibited activation policy, so app-level hiding APIs can't reach it — only the Accessibility API can manipulate its windows cross-process.

Gotchas

  • AX framework bindings live in pyobjc-framework-ApplicationServices (module HIServices) which base pyobjc installs lack — raw ctypes avoids the dependency entirely
  • AXValueCreate and AXIsProcessTrusted work without the Accessibility grant; only Copy/SetAttributeValue fail (error -25204), so trust-gate before enumerating
  • TCC attributes the grant to the exact interpreter binary (e.g. python3.10), not the symlink or the terminal app

Context

Scheduled Playwright scrapers on a macOS desktop that is also used interactively, when the target site's anti-bot rotation forces the WebKit engine.

Revisions (0)

No revisions yet.