Run persistent browsers on your own server and let agents use them from another machine. CloakHub manages CloakBrowser profiles with saved logins, a live browser viewer, and remote automation. Idle browsers stop to free resources and wake when you connect again; their profile data stays on disk.
- Persistent identities: each profile keeps cookies, storage, fingerprint settings, and proxy configuration.
- On-demand browsers: automatic wake-up, configurable idle sleep, and a running-instance limit.
- Human and agent access: use the web viewer for login or manual work and the companion skill for automation.
- Simple deployment: Docker Compose on Linux, with amd64 and arm64 images.
flowchart LR
subgraph client[Client machine]
agent[AI agent]
skill[CloakHub browser skill]
config[client.json and CDP token]
agent --> skill
config --> skill
end
human[Your web browser]
subgraph server[Linux server - Docker Compose]
hub[CloakHub - port 7788]
browser[CloakBrowser instances]
display[KasmVNC - live viewer]
data[(Persistent data volume)]
hub -->|CDP and lifecycle| browser
hub -->|Viewer proxy| display
display --- browser
hub -->|Profile metadata| data
browser -->|Cookies and browser storage| data
end
skill -->|CDP over WebSocket| hub
human -->|Web UI and live viewer| hub
browser --> sites[Websites]
The server runs the browsers. The client runs the agent and a small Node/Playwright script supplied by the skill; it needs no local browser, browser license, or MCP server. A client target alias selects the hub URL, exact profile ID, and profile CDP token together.
You need a Linux host with Docker Compose and a CloakBrowser key that permits the
selected browser build. No source checkout or local image build is required. If you
cannot provide a license key, skip ahead to Run the free image.
Create a deployment directory and save this as compose.yml (also available here):
services:
cloakhub:
image: ghcr.io/txchen/cloakhub:0.9.0
restart: unless-stopped
shm_size: 2gb
environment:
CLOAKHUB_LICENSE_KEYS_FILE: /run/secrets/cloakbrowser-keys
CLOAKHUB_DATA_DIR: /data
CLOAKHUB_HOST: 0.0.0.0
CLOAKHUB_PORT: "7788"
CLOAKHUB_DISK_CACHE_SIZE_MB: "${CLOAKHUB_DISK_CACHE_SIZE_MB:-256}"
CLOAKHUB_DEFAULT_TIMEZONE: "${CLOAKHUB_DEFAULT_TIMEZONE:-UTC}"
CLOAKHUB_DEFAULT_LOCALE: "${CLOAKHUB_DEFAULT_LOCALE:-en-US}"
CLOAKHUB_AUTH_TOKEN: "${CLOAKHUB_AUTH_TOKEN:?Set CLOAKHUB_AUTH_TOKEN in .env}"
ports:
- "${CLOAKHUB_BIND_ADDRESS:-127.0.0.1}:7788:7788"
volumes:
- ./data:/data
- ./secrets/cloakbrowser-keys:/run/secrets/cloakbrowser-keys:roCreate .env beside it, replacing the admin password with a value generated by
openssl rand -hex 32. The following example allows access from your private LAN:
CLOAKHUB_AUTH_TOKEN=replace-with-a-random-admin-password
CLOAKHUB_BIND_ADDRESS=0.0.0.0
CLOAKHUB_DEFAULT_TIMEZONE=America/Los_Angeles
CLOAKHUB_DEFAULT_LOCALE=en-USChoose the timezone and locale for your browser's internet exit. Omit
CLOAKHUB_BIND_ADDRESS to bind only to server localhost, for example behind a local
reverse proxy. For internet access, use HTTPS and a proxy supporting WebSocket upgrades.
Set X-Forwarded-Host and X-Forwarded-Proto to the public origin and overwrite
client-supplied forwarded headers. Mount CloakHub at the origin root, not a path prefix.
Prepare storage, then create secrets/cloakbrowser-keys with one CloakBrowser key per line:
mkdir -p secrets data
chmod 700 secrets
# Save your key(s) in secrets/cloakbrowser-keys, then:
chmod 600 .env secrets/cloakbrowser-keys
docker compose pull
docker compose up -d
docker compose logs -f cloakhubOpen http://<server-lan-ip>:7788 (or your HTTPS origin) and sign in with the admin
password. With localhost binding, open http://localhost:7788 on the server or use
your reverse proxy. The image automatically selects amd64 or arm64.
Version 0.9.0 bundles the latest public CloakBrowser binary: 146.0.7680.177.5
on amd64 and 146.0.7680.177.4 on arm64, verified by SHA-256 at build time.
No browser download occurs at startup. The image includes Mac fonts and defaults new
profiles to a macOS identity. No license key is bundled; the standard Hub still requires one. Keep ./data across container replacements: it contains
profile metadata, browser storage, and secrets. See browser builds
and the preview/stable comparison for details.
The same release publishes two images:
| Image | License key | Browser | Notes |
|---|---|---|---|
ghcr.io/txchen/cloakhub |
Required | Public 146 build, baked in and SHA-256 verified | amd64 and arm64 |
ghcr.io/txchen/cloakhub_free |
None | Patched 154 build, baked in and SHA-256 verified | amd64 only |
Both default to a macOS persona, bundle Mac fonts, and otherwise share the same Hub features and settings.
Use the free image when you cannot or do not want to supply a CloakBrowser key. It requires no secret file, does not contact the license service, and does not download a browser at runtime. It is amd64 only; on arm64, use the standard image instead.
The free image sets CLOAKHUB_LICENSE_MODE=none and already defaults
CLOAKHUB_DATA_DIR, CLOAKHUB_HOST, CLOAKHUB_PORT, and
CLOAKHUB_MAX_RUNNING_INSTANCES=10, so the Compose file only needs the settings an
operator typically customizes. The Hub reads no license key, never queries license
capacity, and never downloads a browser. It uses the license-free CloakBrowser build
from the 154.0.8037.57.1 patched release,
verified by SHA-256 at image-build time. Concurrent Browser Instances are capped only
by CLOAKHUB_MAX_RUNNING_INSTANCES (default 10; add it to the Compose environment
to raise it).
Save compose.free.yml as compose.yml in your deployment
directory:
services:
cloakhub-free:
image: ghcr.io/txchen/cloakhub_free:0.9.0
restart: unless-stopped
shm_size: 2gb
environment:
CLOAKHUB_DISK_CACHE_SIZE_MB: "${CLOAKHUB_DISK_CACHE_SIZE_MB:-256}"
CLOAKHUB_DEFAULT_TIMEZONE: "${CLOAKHUB_DEFAULT_TIMEZONE:-UTC}"
CLOAKHUB_DEFAULT_LOCALE: "${CLOAKHUB_DEFAULT_LOCALE:-en-US}"
CLOAKHUB_AUTH_TOKEN: "${CLOAKHUB_AUTH_TOKEN:?Set CLOAKHUB_AUTH_TOKEN in .env}"
ports:
- "${CLOAKHUB_BIND_ADDRESS:-127.0.0.1}:7788:7788"
volumes:
- ./data:/dataCreate .env beside it with an admin password. No secrets/cloakbrowser-keys file or
secrets directory is needed:
CLOAKHUB_AUTH_TOKEN=replace-with-a-random-admin-password
CLOAKHUB_BIND_ADDRESS=0.0.0.0
CLOAKHUB_DEFAULT_TIMEZONE=America/Los_Angeles
CLOAKHUB_DEFAULT_LOCALE=en-USThen start it:
mkdir -p data
chmod 600 .env
docker compose pull
docker compose up -d
docker compose logs -f cloakhub-freeOpen http://<server-lan-ip>:7788 (or your HTTPS origin) and sign in with the admin
password. Keep ./data across container replacements. To pin a different release,
change the image: tag; on a release, latest also points at the free image. The rest
of this guide (profiles, client setup, and CDP) applies unchanged.
- Create a profile in the web UI with an exact ID such as
research. - Open its viewer and sign into any websites the agent should use. Default headed mode supports unattended automation; you do not need to leave the viewer open.
- To protect CDP access, open the profile's ··· menu and select Manage CDP token. In the CDP token section, click Protect with token, then Copy CDP URL to retrieve the generated token. It is not an editable field in Edit settings; the UI does not accept a custom token.
- Give the client the hub origin, profile ID, and CDP token if configured.
There are three separate credentials:
| Credential | Where it belongs | Purpose |
|---|---|---|
| CloakBrowser key | Server secret file | Download and run the browser |
| CloakHub admin password | Operator / management API | Manage profiles and the server UI |
| Profile CDP token | Client config, secret file, or environment | Operate one profile's browser |
If CDP access is protected, the agent needs only the profile CDP token. An admin password does not replace it. A profile without a CDP token has unprotected CDP endpoints, even when admin authentication is enabled.
In a profile's Edit settings page, add arguments under Custom launch arguments, one per line. They apply the next time the browser starts, so restart the profile after changing them. These switches can affect site behavior or the browser fingerprint; test them on a separate profile if compatibility matters.
| Argument | What it does | Notes |
|---|---|---|
--blink-settings=imagesEnabled=false |
Prevents image loading and can reduce page bandwidth. | Image-dependent layouts, maps, CAPTCHA challenges, and buttons may not work correctly. |
--disable-background-networking |
Reduces some browser background network activity. | Does not block requests made by websites; may affect browser background services. |
--disable-notifications |
Suppresses website notification prompts. | Convenience only; does not reduce page traffic. |
--mute-audio |
Mutes browser audio output. | Does not stop audio or video from downloading. |
For automation, Playwright can block image requests for a task. Add the route before navigating; it applies to pages in that profile context while the client is connected:
await context.route("**/*", route =>
route.request().resourceType() === "image"
? route.abort()
: route.continue()
);This is per-client automation behavior, unlike a launch argument, and may change how browser caching works while routing is enabled.
On the client machine, install Node.js 20+ and run:
npx skills add txchen/cloakhub --skill cloakhub-browser --globalSelect your agent clients and the default symlink installation mode when prompted.
The global skill directory is normally ~/.agents/skills/cloakhub-browser in that mode.
Install its pinned Node dependency there:
npm ci --prefix "$HOME/.agents/skills/cloakhub-browser"If you choose copy mode or a different installation location, use the directory
printed by the installer instead. Reload your agent's skills or restart its session.
The skills CLI installs the skill files;
npm ci installs playwright-core without downloading a local browser. After a skill
update, run npm ci in the installed directory again.
Create the client configuration directory:
mkdir -p "$HOME/.config/cloakhub"
chmod 700 "$HOME/.config/cloakhub"Save this as ~/.config/cloakhub/client.json, replacing the example origin and
profile ID with yours:
{
"version": 1,
"defaultTarget": "work",
"targets": {
"work": {
"url": "http://192.168.1.50:7788",
"profile": "research",
"token": "REPLACE_WITH_PROFILE_CDP_TOKEN"
}
}
}Restrict access to the config file, which now contains the CDP token:
chmod 600 "$HOME/.config/cloakhub/client.json"work is a client-local alias; research is the exact server profile ID, not its
display name. url is the hub origin without /api/.... The helper reads the
token directly; keep client.json out of prompts and committed files.
For multiple profiles or servers, add entries under targets, each with its own
url, profile, and credential. The same skill works for all of them:
- No explicit selection: use
defaultTarget. - A request such as “use the personal browser”: the agent passes
--target personal. CLOAKHUB_TARGET=workin the agent's execution environment overrides the default; explicit--targettakes precedence over both.CLOAKHUB_CLIENT_CONFIGselects a different config file. No project-local config is automatically loaded.
The older "tokenFile": "tokens/work" (relative to client.json) and
"tokenEnv": "WORK_CLOAKHUB_CDP_TOKEN" options are still supported if you prefer
them. Use only one of token, tokenFile, or tokenEnv per target. If the profile
has no CDP token, omit all three from its target;
"auth": "none" is optional. For example:
"work": { "url": "http://192.168.1.50:7788", "profile": "research" }This allows anyone who can reach the CDP endpoint to connect, even if the admin UI requires a password. If a token source is configured but its credential is missing, the helper fails rather than falling back to anonymous access. It does not choose an arbitrary profile or fall back to a local browser.
Verify the setup:
node "$HOME/.agents/skills/cloakhub-browser/scripts/browser.mjs" targets
node "$HOME/.agents/skills/cloakhub-browser/scripts/browser.mjs" check --target worktargets lists local connection metadata without reading tokens or contacting the
server. check makes an authenticated CDP connection, wakes the profile if needed,
reports "ok": true, and disconnects. Then ask your agent, for example:
Use the CloakHub work browser to open example.com and tell me the page title.
See the skill for the browser workflow and client setup reference for troubleshooting.
CloakHub exposes standard Chrome DevTools Protocol (CDP) endpoints. Existing CDP-based tools such as Playwright and Puppeteer can connect directly: point their remote-browser connection at your profile's endpoint and, if configured, supply its CDP token. No CloakHub SDK, agent skill, or admin credential is required for browser operations.
Install Playwright and CloakBrowser's client package on your client machine to use its Humanize behavior layer with the remote browser:
npm install playwright-core cloakbrowserhumanizeBrowser() patches the connected browser's existing and new pages. It adds
human-like behavior to supported mouse, keyboard, and scrolling actions; it does not
turn navigation, DOM reads, or arbitrary page.evaluate() calls into human actions.
Save this as browse.mjs. Replace the example origin and profile ID. If the profile
has a CDP token, provide it through CLOAKHUB_CDP_TOKEN; otherwise leave that variable
unset. Omitting the client token does not bypass a token configured on the server.
This example opens Device & Browser Info's bot test,
prints its detection results, and saves a screenshot on the client:
import { chromium } from 'playwright-core';
import { humanizeBrowser } from 'cloakbrowser';
const token = process.env.CLOAKHUB_CDP_TOKEN;
const browser = await chromium.connectOverCDP(
'https://browser.example.com/api/profiles/research/cdp',
{
headers: token ? { Authorization: `Bearer ${token}` } : {},
timeout: 60_000 // Allow time to wake a stopped browser.
}
);
try {
// Recommended for interactive automation: use CloakBrowser's Humanize behavior layer.
await humanizeBrowser(browser, { humanize: true, humanPreset: 'default' });
const context = browser.contexts()[0]; // Reuse the profile's saved login state.
const page = await context.newPage();
try {
await page.goto('https://deviceandbrowserinfo.com/are_you_a_bot', {
waitUntil: 'domcontentloaded', timeout: 60_000
});
// Wait for the site's asynchronous report, not just the initial page load.
const report = page.locator('#jsonResult').filter({ hasText: /"isBot"\s*:/ });
await report.waitFor({ state: 'visible', timeout: 60_000 });
console.log(JSON.stringify(JSON.parse(await report.innerText()), null, 2));
await page.screenshot({ path: 'bot-detection.png', fullPage: true });
} finally {
await page.close(); // Close only the tab this script created.
}
} finally {
await browser.close(); // Playwright disconnects this remote CDP client.
}Run node browse.mjs (with the token in its environment if required). The connection
automatically starts a stopped profile; no separate launch request is needed.
Clients accepting a WebSocket endpoint can use
wss://browser.example.com/api/profiles/research/cdp with the same authorization
header. For a private HTTP deployment, use http:// / ws:// instead.
Inspect the site's isBot verdict and signals such as isAutomatedWithCDP,
isAutomatedWithCDPInWebWorker, isPlaywright, and hasWebdriverTrue. A true
detection signal means that particular check flagged the browser. This test covers
fingerprinting signals, not IP reputation or user behavior; passing it is not a
guarantee against detection on other sites. Results depend on the browser build,
profile settings, automation client, and the site's current checks. If the report
times out, treat the test as incomplete, not as a pass; the site's DOM may also change.
Disconnect when finished so idle shutdown can reclaim resources. Reconnect through the same fixed profile URL after a stop or restart; existing connections and in-flight commands are not automatically resumed. Compatibility follows each library's CDP support; CloakHub does not implement Playwright's separate browser-server protocol. See the API guide for discovery and authentication details.
Select a profile in the sidebar to open its viewer or controls. Use its menu to edit settings, manage its token, or start/stop it. Close viewer disconnects the viewer only. The event log shows starts, failures, idle stops, and capacity events.
Browsers wake automatically through their fixed profile connection URL. The skill disconnects after each script so idle sleep can work. An open CDP connection prevents automatic sleep; simply watching the viewer does not. Sleep retains cookies and browser storage but closes all tabs; the next start opens a new tab. Live JavaScript and unfinished operations are lost.
Multiple clients using the same profile share tabs and login state; aliases do not provide task isolation. Coordinate access or assign separate profiles for parallel work. Explicit Stop/Restart disconnects every client. Delete removes the profile's stored data. Launch-setting edits apply on the next start; deployment defaults affect new profiles only. CloakHub itself does not apply automatic GeoIP or SDK Humanize. The example above opts into Humanize client-side; set timezone and locale explicitly for the browser profile.
Common settings can be added under environment in Compose. Only variables explicitly
referenced by compose.yml are picked up from .env.
| Setting | Default / purpose |
|---|---|
CLOAKHUB_MAX_RUNNING_INSTANCES |
10; browser key quotas may lower the effective limit |
CLOAKHUB_DEFAULT_PLATFORM |
macos in Docker; linux in source runs without an override |
CLOAKHUB_DEFAULT_TIMEZONE / CLOAKHUB_DEFAULT_LOCALE |
Compose uses UTC / en-US; set these to your region |
CLOAKHUB_DISK_CACHE_SIZE_MB |
256 per profile; HTTP cache budget, not a profile disk quota |
CLOAKHUB_BROWSER_VERSION / CLOAKHUB_BROWSER_CHANNEL |
Exact build and channel for the optional runtime installer; unused with a bundled binary |
CLOAKHUB_BROWSER_BIN |
Docker defaults to /opt/cloakbrowser/chrome; override with a mounted binary if needed |
CLOAKHUB_LICENSE_MODE |
required; set none to skip license loading and capacity checks (the free image sets none) |
CLOAKHUB_LICENSE_KEYS_FILE |
Browser keys; see multiple-key configuration |
Pin a full image tag such as 0.9.0 for predictable upgrades. The 0.9 alias follows
patch releases; latest follows releases, and master / sha-* are development tags.
Browser builds stay pinned until you change the selected build or image. The free image,
ghcr.io/txchen/cloakhub_free, follows the same tags.
Before upgrading, stop the service with docker compose stop and back up the entire
./data directory while browsers are stopped. Update the image tag, then run
docker compose pull and docker compose up -d. Retain the old image tag and backup
for rollback; do not assume older browsers can read profiles upgraded by newer builds.