ScalebrowserDOCS

Migrate from Playwright or Puppeteer

Attach Playwright or Puppeteer code to a Scalebrowser profile over CDP, remove the stealth plugin, and move strict targets to direct CDP with human input.

Playwright and Puppeteer can both attach to a Chromium that is already running, and a Scalebrowser profile is one. The first step keeps your script and changes only where the browser comes from. Patchright takes the same call with the same arguments as Playwright.

Start a profile and attach

Start the profile through the daemon, then hand its cdp_ws to your tool. The address is new at every start, so read it from the start response each time. Direct CDP describes the call and the fields it returns.

python
import os
from playwright.sync_api import sync_playwright
from scalebrowser import ScalebrowserClient

sb = ScalebrowserClient(base_url="http://127.0.0.1:8787", token=os.environ["SCALEBROWSER_TOKEN"])
PROFILE_ID = "prf_7d2a…"   # an existing profile; sb.create_profile(...) makes a new one

started = sb.start_profile(PROFILE_ID)
try:
    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp(started.cdp_ws, no_defaults=True)
        context = browser.contexts[0]            # the profile's own context
        page = context.pages[0] if context.pages else context.new_page()
        page.goto("https://example.com")
        print(page.title())
        browser.close()                          # disconnects; the browser keeps running
finally:
    sb.stop_profile(PROFILE_ID)

puppeteer-core is enough for Puppeteer: it attaches and never downloads a browser of its own.

Five things that change when you attach

  • Use the existing context, not a new one. browser.contexts[0] is the profile: its cookies, its storage, its logins. new_context() creates a separate incognito context that is thrown away when you disconnect, so nothing done there reaches the profile.
  • Tell your tool to leave the context alone. Without no_defaults=True (Python) or noDefaults: true (Node), available since Playwright 1.60, Playwright points downloads at a temporary folder and turns on focus and media emulation for the default context. Puppeteer without defaultViewport: null gives every page an 800 by 600 viewport that no longer matches the window.
  • Launch options do not apply. proxy, args, headless and user_data_dir belong to a launch, and nothing is launched. Store the proxy on the profile (Proxies & exits) and pass headless to the start call.
  • Closing disconnects. browser.close() in Playwright and browser.disconnect() in Puppeteer leave the browser running. Stop the profile through the daemon, or it keeps holding one of your plan's browsers.
  • The address is new every start. Never cache cdp_ws across a stop and a start.

Remove the stealth plugin

playwright-stealth in Python and playwright-extra with puppeteer-extra-plugin-stealth in Node patch browser properties with scripts. On a profile they do harm:

  • Their values replace the profile's. Both ship fixed defaults: languages en-US, platform Win32, a WebGL vendor Intel Inc. with the renderer Intel Iris OpenGL Engine, and a rewritten user agent. A profile reports the GPU and display of the machine it runs on, and the machine's own rendering can be compared against what the plugin claims.
  • They mostly miss the attached context. Both hook the creation of new contexts and pages, so the default context of an attached profile is not patched unless you apply the plugin to it yourself (read from their source, September 2026).

The playwright-stealth README says it plainly: "Don't expect this to bypass anything but the simplest of bot detection methods." Delete the import and the call; the profile needs neither.

What the page still sees

Attaching changes the browser, not how your script types and clicks. Three habits of these tools are visible to the page, measured on our own bench with every tool driving the same form:

ToolWhat the page receives
Playwright fillThe text arrives in one piece, with no key events at all
Playwright and Puppeteer typeCapital letters with no Shift key pressed
Puppeteer clicksA pointerdown with pressure 0 while a button is held, where the specification requires 0.5

You can see what your own tool leaves behind on /ai-agent-check, without an account.

Move strict targets to the SDK driver

For a site that grades input, drive the page with the SDK instead. It speaks CDP directly, never calls Runtime.enable, and sends every click and keystroke through the daemon's human input layer. A selector becomes a box, and the click lands inside it:

python
import os
from scalebrowser import ScalebrowserClient

sb = ScalebrowserClient(base_url="http://127.0.0.1:8787", token=os.environ["SCALEBROWSER_TOKEN"])

BOX = """(() => {
  const r = document.querySelector('#email').getBoundingClientRect();
  return { x: r.x, y: r.y, width: r.width, height: r.height };
})()"""

with sb.launch("prf_7d2a…") as page:
    page.navigate("https://example.com/login")
    box = page.evaluate(BOX, isolated=True)
    page.humanize_click(box["x"] + box["width"] / 2, box["y"] + box["height"] / 2)
    page.humanize_type("name@example.com")

isolated: true reads the box from an isolated world, so the page's own scripts never see the query. launch starts the profile and stops it when the block ends.

If an AI agent is doing the driving rather than your code, skip the script entirely: the MCP server gives the agent the page as a text map and the same input layer behind every action.

Next