| name | deva-cloak |
| description | Drive CloakBrowser stealth Chromium inside the deva cloak container - a headed, anti-detection browser for scraping, automation, or checking a site the way a real browser sees it. Use when the task needs a browser that bypasses bot detection (Cloudflare, FingerprintJS, reCAPTCHA scoring), or when the user asks to browse/scrape a protected site from inside deva. Only applies in the cloak image (verify with `command -v cloakbrowser`; DEVA_CLOAK=1 is the marker); the base/rust images have plain Playwright instead. |
deva-cloak: stealth browser in the deva cloak container
CloakBrowser is a custom-compiled Chromium with source-level fingerprint
patches. Same Playwright API, different binary -- detection sites score it as
a real browser because it is one. This image runs it headed by default on a
virtual display, which passes checks that headless fails even with the patches.
Am I in the cloak context?
This skill only applies when the browser is actually present. ALWAYS run
this check fresh -- never conclude from conversation context, a resumed
session, or $DEVA_CLOAK alone (env markers can be scrubbed or stale;
the binary is the ground truth):
command -v cloakbrowser >/dev/null && cloakbrowser info --quick
If that fails, you are in the base or rust image -- there is no CloakBrowser
here. (DEVA_CLOAK=1 is baked into the cloak image as a convenience marker,
but treat it as a hint, not the test.) Tell the user to relaunch with
deva.sh -p cloak <agent>, or use the plain Playwright install (rust image)
instead. Do not try to npm install cloakbrowser yourself; the binary is
baked into the cloak image and a runtime install would download ~200MB and
defeat the point.
What is baked in
- Wrapper +
playwright-core live in $CLOAK_APP_DIR (/opt/cloak).
- Chromium binary is pre-downloaded, cache at
/opt/cloakbrowser, auto-update
off -- fully offline, no network needed to launch.
- Free binary = Chromium 146 (58 fingerprint patches). No Pro license.
Good enough for most sites and for viewing your own apps; the newest
detection tricks (latest Cloudflare/DataDome) may still win. Pro (146->150,
71 patches) needs
CLOAKBROWSER_LICENSE_KEY at runtime -- no rebuild.
- Headed display: Xvfb on
:99 + openbox, started by the entrypoint.
x11vnc for VNC into that display (interactive logins). Start it on demand
(x11vnc -display :99 -rfbport 5900 -bg); on OrbStack/Linux the Mac reaches
it directly at the container IP -- no port publish, no launch flag.
cloak-browserd always-on browser daemon
(/usr/local/lib/cloak/cloak-browserd.mjs); OPT-IN, runs only with
--cloak-browser, exposes CDP at CLOAK_CDP_ENDPOINT. Independent of VNC.
Running a script
CloakBrowser is ESM-only, so run the script from $CLOAK_APP_DIR where its
node_modules resolves. Write output to absolute workspace paths.
cd "$CLOAK_APP_DIR"
node --input-type=module <<'EOF'
import { launch } from 'cloakbrowser';
// Headed is the whole point of this image -- keep headless:false for
// detection-heavy sites. Flip to true only for speed on trusted targets.
const browser = await launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log(await page.title());
await browser.close();
EOF
The returned object is a normal Playwright Browser -- newPage,
newContext, screenshot, etc. all work unchanged.
Options that matter
Pass to launch({ ... }):
headless: false -- default posture here; some sites detect headless even
with the C++ patches.
humanize: true -- human-like mouse curves, per-char typing, scroll. Turn on
for behavioral detection (deviceandbrowserinfo, reCAPTCHA v3 scoring).
proxy: 'http://user:pass@host:port' or 'socks5://...' -- residential IPs
pass reputation checks that datacenter IPs fail. Add geoip: true to match
timezone/locale to the proxy exit IP.
args: ['--fingerprint=12345'] -- fixed seed = a consistent returning-visitor
identity across runs (better for reCAPTCHA v3 than a fresh identity each time).
Recommended stack for a hard anti-bot target:
launch({ headless: false, humanize: true, proxy: '<residential>', geoip: true }).
Authenticated sites: log in over VNC
You cannot copy the host browser's cookie DB in -- Chrome encrypts cookie
values with an OS-keychain key the Linux container can't reach, so the blobs
are undecryptable here. Instead the human logs in once over VNC, watching a
browser YOU (the agent) drive on the cloak display -- same browser, same
profile, persisted to the host.
The human launches normally -- no VNC flag needed on OrbStack/Linux:
deva.sh -p cloak claude
You (the agent) start VNC on demand when a login is needed. OrbStack (and
native Linux Docker) route the container straight from the host, so a server
you start INSIDE the container is reachable from the Mac with no port publish
and no launch flag:
x11vnc -display :99 -rfbport 5900 -forever -shared -bg
Then tell the human how to connect. The container is reachable from the Mac at
its OrbStack address -- orb list on the Mac gives the name/IP, then
open vnc://<container-ip>:5900 (or <name>.orb.local:5900), built-in Screen
Sharing. No -p, no recreate.
(--cloak-vnc still exists, but only for plain Docker Desktop, where the
container IP is NOT routable from the host and a published loopback port is the
only way in. On OrbStack you don't need it -- and it does not have to be set at
launch.)
You (the agent) open a headed browser on the display and drive it. Use a
persistent context on the mounted profile so the login survives:
cd "$CLOAK_APP_DIR"
node --input-type=module <<'EOF'
import { launchPersistentContext } from 'cloakbrowser';
const ctx = await launchPersistentContext({
userDataDir: process.env.DEVA_CLOAK_PROFILE_DIR || '/home/deva/.cloak-profile',
headless: false, // renders on :99 so the human sees it over VNC
});
const page = ctx.pages()[0] || await ctx.newPage();
await page.goto('https://SITE/login');
// Tell the human: open the printed vnc:// URL and log in (MFA/captcha).
// Wait for the post-login page, then continue -- you hold the same context.
await page.waitForURL('**/app**', { timeout: 300000 });
console.log('logged in:', await page.title());
await ctx.close(); // profile saved to the mounted host dir
EOF
The human opens vnc://127.0.0.1:PORT (macOS: open vnc://127.0.0.1:5901,
built-in Screen Sharing), sees your browser, and logs in. Because you hold the
same persistent context, the session is yours immediately and -- via the
mounted profile -- survives to the next run, even headless with no VNC.
Optional: always-on shared daemon (--cloak-browser). For a browser that
stays up across agent runs (so the human can log in even when no agent is
active), launch with --cloak-browser. deva starts one persistent headed
browser and sets CLOAK_CDP_ENDPOINT; attach to it instead of launching your
own -- do NOT launch() a second browser, it would miss the login:
import pw from 'playwright-core';
const b = await pw.chromium.connectOverCDP(process.env.CLOAK_CDP_ENDPOINT);
const ctx = b.contexts()[0]; // the daemon's profile, NOT b.newContext()
const page = ctx.pages()[0] || await ctx.newPage();
await page.goto('https://SITE/app'); // already logged in
await b.close(); // detaches; the daemon browser stays up
CLOAK_CDP_ENDPOINT is set only when the daemon runs (--cloak-browser); its
absence means there is no daemon and you should launch your own browser as
above.
Use goto(), not setContent(): on a fresh daemon page setContent waits for
a load event that never fires and times out. page.goto('data:text/html,...', {waitUntil: 'domcontentloaded'}) is the working equivalent for injected markup.
Persistence / what to mount. --cloak-vnc (and --cloak-browser)
auto-mounts ~/.config/deva/cloak-profile (host) to the Chromium userDataDir
/home/deva/.cloak-profile. That one dir is the whole session -- cookies,
localStorage, IndexedDB, service workers, Cache, History, Login Data. It
survives container removal, so a login done once is reused by every later run
(even headless, no VNC). Override the host location with
DEVA_CLOAK_PROFILE_DIR=/path deva.sh -p cloak --cloak-vnc ..., or mount
your own dir at /home/deva/.cloak-profile (your mount wins). Do NOT mount
/opt/cloakbrowser -- that is the baked binary, not session data.
Gotcha: only cookies with an expires survive a profile reopen -- session
cookies (no expiry) are memory-only. Real logins persist via the site's
long-lived "remember me" / refresh-token cookie, which carries an expiry, so
genuine sessions survive (verified).
storageState alternative (no daemon/VNC): if you captured Playwright
storage state elsewhere (cookies + localStorage as plaintext JSON) and mounted
the file, load it into a one-off context -- note the nesting:
node --input-type=module <<'EOF'
import { launchContext } from 'cloakbrowser';
// storageState MUST be under contextOptions -- launchContext feeds it to
// newContext(). Top-level storageState is silently dropped and you get an
// UNauthenticated browser (verified against cloakbrowser 0.4.x).
const ctx = await launchContext({ headless: false, contextOptions: { storageState: '/home/deva/.cloak-auth/SITE.json' } });
const page = await ctx.newPage();
await page.goto('https://SITE/app');
await ctx.close();
EOF
SECURITY -- the profile dir (and any storage-state file) is a live bearer
credential: whoever holds it holds the session. This container runs the agent
with permissions disabled. VNC is auto-passworded; keep it that way. Only keep
the single site the task needs, and never print, log, copy, or write these
files back to a shared/primary account. Treat them as secrets.
CLI (no script needed)
cloakbrowser info
cloakbrowser info --quick
Gotchas
- Bare
import 'cloakbrowser' only resolves when cwd is $CLOAK_APP_DIR
(Node ESM resolves bare specifiers from cwd for --input-type=module). From
elsewhere, import the absolute path:
import(process.env.CLOAK_APP_DIR + '/node_modules/cloakbrowser/dist/index.js').
- Do not run
npx cloakbrowser ... -- the wrapper is not global; use the
cloakbrowser command (symlinked) or the $CLOAK_APP_DIR binary.
- For reCAPTCHA scoring, use native
setTimeout, not page.waitForTimeout()
(the latter sends CDP traffic reCAPTCHA detects); use page.type() not
page.fill() so keystroke events fire.
- CloakBrowser prevents CAPTCHAs; it does not solve them. Bring your own proxy.