| name | playwright-cli-cloakbrowser |
| description | Drive CloakBrowser Manager stealth profiles with @playwright/cli over CDP. Use for browser automation that needs a persistent logged-in session, anti-detect fingerprints, or to pass Cloudflare - attach reuses the profile's cookies and stealth where a fresh browser does not. |
| metadata | {"version":"0.3.1","upstream":"@playwright/cli@0.1.15"} |
| allowed-tools | Bash(playwright-cli:*) Bash(curl:*) Bash(docker:*) |
playwright-cli on CloakBrowser stealth profiles
CloakBrowser Manager runs isolated stealth Chromium profiles - unique fingerprint, optional proxy, persistent cookies, built-in noVNC live view - in a single Docker container. Every running profile exposes a CDP endpoint. playwright-cli attach connects to the profile's persistent context (contexts[0]), so automation inherits the profile's logins and stealth fingerprint; playwright-cli open would launch a plain local browser with neither. Always attach for this setup.
No Manager running yet? See Setting up the Manager below.
For the full command reference, run playwright-cli --help and open the Agent skill: path it prints (the official skill bundled with @playwright/cli). This file only covers the CloakBrowser-specific setup and gotchas.
Connect
playwright-cli attach --cdp=<CDP_URL>
playwright-cli attach --cdp=http://localhost:8080/api/profiles/<profile-id>/cdp
playwright-cli goto https://example.com
playwright-cli snapshot
playwright-cli snapshot --depth=6
playwright-cli click e15
playwright-cli --raw eval "document.title"
playwright-cli detach
- The profile must be running first - click Launch in the Manager GUI, or
curl -X POST http://localhost:8080/api/profiles/<profile-id>/launch.
--cdp=<url> and --cdp <url> both work.
attach creates an implicit session named default and prints a --s=default hint - that flag is only needed when juggling multiple sessions; plain commands use the default session. -s=<name> opens a second isolated session on the same CDP (e.g. -s=probe for in-page fetch checks while default drives the page).
- playwright-cli writes a
.playwright-cli/ scratch dir (snapshot .yml files, console logs) into the cwd - run from a scratch directory or gitignore it.
- Never
close / close-all / kill-all on an attached stealth profile - that can stop the shared browser and its session. Use detach to leave it running.
Logging into a site
The profile persists cookies, localStorage, and cache across restarts. To establish a login: open the Manager GUI at http://localhost:8080, view the running profile in the built-in noVNC viewer, and sign in there once. playwright-cli then drives the same session logged-in - the VNC view and the CDP connection share one browser.
Importing an existing login (two limits worth knowing):
- Anti-automation sites (Google, Facebook) may flag the CDP-driven login and not persist a durable session even though the form submits. Bypass: import a real-browser session as Playwright storage-state (
playwright-cli state-save/state-load, or capture with npx playwright codegen --save-storage=auth.json <url>) instead of logging in through VNC.
- A session is often IP-bound: a cookie/state minted at your real location, replayed from a profile whose proxy egress is a different geo, gets forced back to login + 2FA. Transfer only works when the profile's proxy geo matches where the session was created.
Gotchas (all observed in practice)
attach hangs ~30 s, then times out - the profile has a stale or heavy pre-existing tab that never finishes Playwright's connectOverCDP page-init handshake. Fix: close extra tabs via the noVNC view (or stop/relaunch the profile), then re-attach - drops to under a second. A clean about:blank profile attaches instantly.
- New-tab creation can be rejected ("Failed to open a new tab") - the stealth browser may forbid
Target.createTarget in some states. Prefer navigating the current tab (goto) over tab-new. If a page closes or replaces its own tab (challenge redirects, heavy detail pages), the driver desyncs - just re-attach and continue.
- JS-rendered content (prices, dynamic cards) often is not in the a11y
snapshot - scrape the DOM with eval: playwright-cli --raw eval "[...document.querySelectorAll('[data-card]')].map(e=>e.innerText)".
- Heavy detail pages are slow through a profile proxy and can timeout or desync. Read what you need from the list/results page when possible; treat opening detail pages as best-effort.
- Cookie/consent overlays block clicks ("element covered"). Dismiss via JS:
playwright-cli eval "document.querySelector('#onetrust-accept-btn-handler')?.click()".
- Use
--raw whenever scraping or piping - without it output is wrapped in markdown header blocks (### Result, ### Page, ### Snapshot - varies by command), which mangles piping. Even with --raw, eval values come back JSON-serialized (strings quoted).
- "Logged in" can be false. A CDP-attached context may render a login form (geo often defaults to US). Verify auth with an
eval render check - playwright-cli --raw eval "!!document.querySelector('[href*=logout], [aria-label*=account]')" - not cookie-get, which can read the automation's own blank page and false-negative.
goto may throw TimeoutError on heavy JS sites (it waits on domcontentloaded) yet a follow-up snapshot/eval still returns usable content - a nav timeout is not fatal, retry the read. (Extends gotcha 4.)
click "<CSS selector>" often fails to match even when the element exists. Prefer snapshot eN refs, or click via JS: playwright-cli eval "document.querySelector('...')?.click()".
- To wait, poll - do not
sleep. Loop on in-page state via eval (document.readyState, element presence) with a bounded max; cap any Cloudflare "Just a moment..." wait explicitly, it is otherwise unbounded.
- Browsing a logged-in session can log you out mid-run - anti-automation defenses fire on real activity. Treat a mid-run logout as a site defense, not a tool bug; re-establish the session and slow down.
Patterns for logged-in & JS-heavy sites
- Reverse-engineer the site's own API. From the logged-in context, call same-origin authenticated endpoints directly - far more robust than DOM scraping:
playwright-cli -s=probe --raw eval "(async()=>await (await fetch('/api/whatever',{headers:{'X-Requested-With':'XMLHttpRequest'}})).json())()"
Discover the endpoints by monkeypatching fetch first: eval "window.__u=[];const f=fetch;window.fetch=(...a)=>{window.__u.push(a[0]?.toString?.()||a[0]);return f(...a)}", exercise the page, then read window.__u.
- Use CDP-level network inspection when in-page hooks fall short. A page reload wipes an injected fetch/XHR hook, and service-worker / iframe traffic is invisible to a main-context hook. Use
playwright-cli requests then playwright-cli --raw response-body <id> to capture what the page actually fetched.
- Lazy-loaded lists: scroll-until-count, not fixed sleeps. Loop: scroll, poll the item count, stop on no-growth OR target OR max-rounds. For content inside a scrollable element, scroll that element's
scrollHeight, not the window:
playwright-cli --raw eval "(async()=>{let p=0;for(let i=0;i<30;i++){scrollTo(0,document.body.scrollHeight);await new Promise(r=>setTimeout(r,700));const n=document.querySelectorAll('[data-card]').length;if(n===p)break;p=n}return p})()"
- Retry hard blocks with a fresh exit IP. On 403/429/503/empty content, retry once against a fresh context so a rotating proxy picks a new exit IP (only helps if the profile's proxy rotates).
Minimal working pattern
CDP=http://localhost:8080/api/profiles/<profile-id>/cdp
playwright-cli attach --cdp=$CDP
playwright-cli goto "https://www.amazon.com/s?k=iphone+16+pro+max"
playwright-cli --raw eval "[...document.querySelectorAll('[data-asin]')].slice(0,5).map(e=>e.innerText.split('\n')[0]).join(' | ')"
playwright-cli detach
Setting up the Manager (local laptop)
Requirements: Docker 20.10+, ~2 GB disk (the image bundles the CloakBrowser binary; the Manager is ready seconds after start), ~512 MB RAM per running profile.
docker run -d -p 8080:8080 -v cloakprofiles:/data cloakhq/cloakbrowser-manager
Binds to localhost. If 8080 is taken, map another host port (-p 18080:8080) and adjust every URL in this file accordingly. Profiles, cookies, and session data live in the cloakprofiles volume and survive container updates (docker pull cloakhq/cloakbrowser-manager + recreate). By default there is no authentication - fine for local use; add -e AUTH_TOKEN=your-secret-token to protect it, and pass Authorization: Bearer <token> on API calls.
Open http://localhost:8080 - the Manager web GUI. Create a profile (set proxy, timezone, and locale together if you need specific geo) and click Launch. Or headless via API:
curl -s -X POST http://localhost:8080/api/profiles \
-H 'Content-Type: application/json' \
-d '{"name": "agent", "platform": "macos"}'
curl -s -X POST http://localhost:8080/api/profiles/<profile-id>/launch
curl -s http://localhost:8080/api/profiles
The noVNC live view is built into the GUI: click the running profile to watch and interact with the browser in a tab. Use it to log into sites once, watch automation live (VNC and CDP share the same browser), and clear stuck tabs when attach hangs (gotcha 1).
Playwright/Puppeteer scripts work against the same CDP URL:
const { chromium } = require("playwright");
const browser = await chromium.connectOverCDP(
"http://localhost:8080/api/profiles/<profile-id>/cdp"
);
const page = browser.contexts()[0].pages()[0];
await page.goto("https://example.com");
Remote Manager on a Linux VM over SSH
The same setup runs on any Linux VM you can SSH into - a VPS, home server, or cloud instance. Size it at ~512 MB RAM per running profile plus ~1 GB for the Manager itself.
On the VM:
curl -fsSL https://get.docker.com | sh
docker run -d --restart unless-stopped --name cloakbrowser \
-p 127.0.0.1:8080:8080 -v cloakprofiles:/data cloakhq/cloakbrowser-manager
On your laptop:
ssh -L 8080:localhost:8080 user@vm
Everything above now works unchanged: the GUI, the noVNC login flow, and the CDP endpoint all ride the same tunneled port, so playwright-cli attach --cdp=http://localhost:8080/api/profiles/<id>/cdp is identical to the local case. Binding to 127.0.0.1 on the VM makes the SSH tunnel the auth boundary, so AUTH_TOKEN stays unnecessary.
If you must expose the Manager directly instead (no tunnel), set AUTH_TOKEN and put it behind an HTTPS reverse proxy - but note that playwright-cli attach cannot inject a Bearer header on the CDP WebSocket, so a token-protected Manager needs a small local proxy in front that injects the header. Prefer the tunnel.
Notes
- Egress IP is whatever the profile's proxy is set to (or the Docker host's IP with no proxy). Sites see that location for geo, currency, and language - configure the profile's proxy, timezone, and locale together in the Manager. To force English on a geoip'd EU marketplace, a
&language= URL param is not enough; override the header (route/run-code with Accept-Language: en-GB,en;q=0.9).
- One profile = one persistent identity. Use separate profiles for separate accounts; that is the point of the Manager.
- CloakBrowser is the stealth Chromium engine; cloakbrowser.dev is the project site. The Manager GUI is MIT; the CloakBrowser binary has its own license.