Skip to content

Playwright & Puppeteer

Clearcote is just Chromium, so it works as a drop-in browser for the automation tools you already use.

The SDK way (recommended)

The clearcote package (npm, PyPI and NuGet) turns identity options into named arguments and returns ordinary Playwright objects. It downloads and SHA-256-verifies the browser on first use — the open build without a licence key, the latest licensed build with one (see Installation) — and applies the defaults described further down.

# pip install clearcote
from clearcote import launch

browser = launch(fingerprint="seed-123", platform="windows", brand="Chrome")
page = browser.new_page()
page.goto("https://example.com")
browser.close()

Current SDK: 0.67.1. Since 0.23, launch() in Python and Node runs on a real, throwaway profile directory (deleted on close) instead of incognito, so the profile-shaped surface matches a real Chrome and widevine: true can load the DRM module. It hands back a browser-like handle: newPage() works as usual, but newContext() returns that same profile context rather than an isolated one. For separate cookie jars, launch separate browsers; pass ephemeralProfile: false / ephemeral_profile=False for the old incognito Browser, or userDataDir / user_data_dir to keep the profile.

The async API (clearcote.async_api) takes the same persona and proxy options inside an asyncio loop and returns Playwright async objects; its launch() is incognito, so use launch_persistent_context() for a profile (and for widevine=True). The .NET SDK has everything the Python and Node SDKs have as a library (since 0.58.0): LaunchEphemeralProfileAsync (recommended — the .NET LaunchAsync is incognito and can't fit a headless window), LaunchPersistentContextAsync, ServeAsync, verified download, licensing, Geoip, Widevine, human input (the Humanize launch option, or explicit HumanClickAsync / HumanTypeAsync / HumanFillAsync / HumanSelectOptionAsync calls), saved profiles and LocalProfile = "auto", CheckRenderCoherenceAsync and the agent helpers (LaunchAgentAsync / RunAgentTaskAsync). The clearcote command-line tools come with the Python and Node packages only. For full copy-paste workflows, see Examples.

Puppeteer and other CDP clients (serve)

The SDK has no Puppeteer launcher. Instead, serve() starts Clearcote with the SDK's launch settings (persona, proxy, defaults) and a CDP endpoint on loopback, and any CDP client attaches to it — Puppeteer, Playwright's connectOverCDP, browser-use, Crawl4AI, Stagehand. It works with the licensed build, and nothing adds --enable-automation. humanize is Playwright-side, so it doesn't apply to a client attached this way.

import { serve } from "clearcote";
import puppeteer from "puppeteer-core";

const srv = await serve({ fingerprint: "seed-123", platform: "windows" });
const browser = await puppeteer.connect({ browserURL: srv.cdpUrl, defaultViewport: null });
const page = await browser.newPage();
await page.goto("https://example.com");
await browser.disconnect();
await srv.close();

Headless, serve() gives the browser a real-size display and fits its window to the work area before any client attaches (0.31+); pass windowSize / window_size for a smaller window. From a shell, clearcote serve does the same as a standing service — and can hand each connection its own browser, with the identity, proxy, timezone and language taken from the connection URL. See Deployment.

Driving the binary directly (open build)

You can also point your own launcher at the open build with executablePath (Node) or executable_path (Python) and pass identity options as args. The licensed build doesn't start this way — it needs the licence token the SDK checks out, so use launch() or serve() above. Drop --enable-automation as the SDK does (Puppeteer and older Playwright versions add it; it switches Chromium into its automation mode). None of the SDK's defaults (language, WebRTC policy, window geometry) apply on this route.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(
        executable_path=r"C:\clearcote\chrome.exe",
        headless=False,
        ignore_default_args=["--enable-automation"],
        args=[
            "--fingerprint=seed-123",
            "--fingerprint-platform=windows",
            "--timezone=America/New_York",
        ],
    )
    page = browser.new_page()
    page.goto("https://abrahamjuliot.github.io/creepjs/")
    browser.close()

Resolve or prefetch the verified binary

The SDK resolves the browser in this order: an explicit executablePath / executable_path, then CLEARCOTE_BINARY, then a version you asked for (or CLEARCOTE_BROWSER_VERSION), then the licensed build if a licence key is found (the licenseKey option, CLEARCOTE_LICENSE_KEY, or ~/.clearcote/license.key), and finally the open build pinned in this SDK version. A path you supply is checked for missing or truncated files before launch. Call download / executable_path when you want to warm the cache without launching, use a custom cache directory, or opt in to the newest open build on GitHub at runtime.

from clearcote import download, launch

chrome = download(cache_dir=r"C:\clearcote-cache", auto_update=True)
browser = launch(executable_path=chrome, fingerprint="seed-123")

Pinned mode verifies the SDK's baked-in SHA-256 values. autoUpdate / auto_update is opt-in and verifies the release checksum manifest; when gpg is available, it also checks the signed manifest against the pinned Clearcote signing-key fingerprint. It applies to the open build only: with a licence key, download() fetches the current licensed build instead (choose one with version or releaseChannel — see choosing a build). In .NET, DownloadAsync fetches the open build only; prefetch the licensed one with ExecutablePathAsync(new LaunchOptions { LicenseKey = … }).

Coherent by default (no extra flags)

Whatever you launch with, the engine keeps the secondary surfaces in agreement with the persona you chose — on a Windows persona, with what a real Chrome-on-Windows desktop reports. The persona's platform defaults to your host OS, so on a Linux host these follow a Linux persona unless you pass platform. WebGL getParameter limits (WebGL1 + WebGL2) report the persona's values, capped at what this machine's GPU can actually deliver, and UNMASKED_RENDERER / UNMASKED_VENDOR are session-constant (the same GPU on every site, tracking the persona). On a Windows persona, navigator.getBattery() reports a desktop on mains power, navigator.connection a residential connection, and AudioContext the matching Windows-WASAPI sample rate and latency. getScreenDetails() reports a single monitor and @media (pointer: fine) / (hover: hover) match a desktop with a mouse.

Checked on 153 r26, headed on Windows (September 2026): BrowserScan reports no bot and CreepJS shows 0% stealth. Behind a proxy, add geoip so the timezone and WebRTC match the exit.

Coherent defaults (overridable)

The SDK applies a few stealth-correct defaults so the obvious tells don't slip through:

  • Headed launches default to no emulated viewport (viewport: null / no_viewport=True) so window.innerWidth tracks the real OS window — an emulated 1280×720 sitting on a real window is an impossible-window tell. Pass an explicit viewport to override. On a Linux server without a window manager (Xvfb, your own container), where --start-maximized does nothing, a headed window is fitted to the screen's work area before the first page, like a maximized desktop window (SDK 0.42.0).
  • Headless launches get a realistic display and a window fitted to it before the first page: the persona's screen, or, without a persona, a real desktop size. From SDK 0.42.0 that holds on every platform even when a persona does not take effect, so a page never sees the engine's bare 800×600 headless surface. A viewport or screen option of your own turns this off, and your own --screen-info sets the display yourself. Your own --window-size, --window-position or --start-maximized keeps the display and skips only the fit; in serve() a bare --window-position keeps the display too, and only a size switch or --screen-info leaves geometry entirely to you.
  • One WebGL surface on Linux: on a Linux host, a Linux persona's WebGL renders through Mesa over EGL headless and headed alike (SDK 0.42.0; the host needs libegl1), so a page sees one renderer string, extension list and set of limits either way, in the OpenGL ES form most real Linux machines report. Without EGL a headed launch uses Mesa's desktop GL, and a Windows persona renders through SwiftShader. Your own --use-angle or --use-gl in args wins, and a headless launch still gets --ignore-gpu-blocklist with it, so WebGL keeps working.
  • Light or dark comes from the persona on the licensed build from 154 r32: contexts the SDK creates leave Playwright's colour-scheme emulation off (SDK 0.41.0+), where Playwright would otherwise force light. Pass colorScheme / color_scheme to emulate one anyway.
  • The persona stays off the command line on the licensed build from 154 r32: the seed, the persona overrides and proxy credentials reach the browser through its environment, not its process-list entry (SDK 0.41.0+; personaEnv: false to keep the command line). See Launch options.
  • WebRTC defaults to disable_non_proxied_udp, so no UDP leaves around the proxy and your machine's own address stays private. Only your own --webrtc-ip-handling-policy in args overrides it — and, on the open build, webrtcIp / geoip, which let WebRTC UDP leave on your own connection again (the licensed build blocks WebRTC UDP regardless). With that policy and no webrtcIp, a page gets no ICE candidates at all — behind a proxy, pass geoip (or webrtcIp) so WebRTC reports the proxy's address, or socks5Udp to carry real UDP through a SOCKS5 proxy.
  • Behind a proxy, QUIC / HTTP-3 is off, as it is for a real Chrome behind a proxy — so no UDP is attempted around it.
  • The persona's platform defaults to your host OS and the brand to Google Chrome. With no timezone and geoip turned off, the timezone follows the language (en-US → New York); geoip is on by default since SDK 0.66.0.
  • Proxy credentials go to the browser, not to Playwright, where the engine supports it: SOCKS5 always (Playwright can't authenticate SOCKS5 at all), HTTP(S) on 151 r19+ so the page cache stays on (Python & Node, and .NET since 0.58.0). See below.
  • humanize runs an actionability pre-flight before each trusted click (visible / enabled / stable + an elementFromPoint covered-by check) and falls back to the native click, so a trusted click never fires under an overlay or mid-animation.
Tip: derive the seed from your own account/tenant id so each identity is reproducible — same seed, same browser fingerprint, every time. See the full switch list under Fingerprint flags.