One persistent stealth browser per profile, driven by CLI verbs over --json — with a registry where AI agents save, compose, and compound reusable automations.
Sessions on one profile must be serialized: cookie injection across contexts is the burn vector, the jar lives in the browser, and open is single-writer by construction. Agents get verbs, not REST — every failure is a JSON document with a nonzero exit, never a traceback.
Two engines, one CLI:
| obscura (default) | cloakbrowser (opt-in Chromium) | |
|---|---|---|
| Install | downloaded by ./install.sh |
bring your own (chromium, chrome, brave, edge) |
| RAM per live profile | ~65 MB | ~1.2 GB |
| Cold launch | ~0.25 s | ~0.4 s |
| Downloads / History | fail fast with a fix (see below) | full CDP support |
| Multi-tab | one page per profile (use profiles) | real tabs |
Requires python ≥ 3.10.
git clone https://github.com/ishan-parihar/cloakctl && cd cloakctl
./install.sh # cloakctl + obscura binary (pipx when present, else an isolated venv)
cloakctl doctor # engines, locks, memory, evolution hints
skills/cloakctl/scripts/smoke.sh # 11-check end-to-end on an isolated state dirEach installer run checks the latest Obscura release, verifies its GitHub SHA-256 digest, smoke-tests the candidate, and atomically replaces the managed binary only after validation.
Opt out of the obscura download with ./install.sh --engine cloakbrowser (needs a Chromium-family browser on PATH; override the binary with CLOAKCTL_BROWSER).
The same operations as MCP tools over stdio — install gives you both binaries:
{"mcpServers": {"cloakctl": {"command": "cloakctl-mcp", "args": []}}}25 tools named cloakctl_{verb}. Core failures arrive as tool errors
with an error: line plus a help: line naming the fixing command — agents
self-correct without a retry ladder. Right default for harnesses that speak
MCP; the CLI for shell loops and pipes.
install.sh detects and wires installed harnesses (best-effort, never fatal). Manual setup:
| Harness | Command |
|---|---|
| hermes-agent | ./install.sh auto-syncs integrations/hermes/browser-cloakctl/ → ~/.hermes/hermes-agent/plugins/browser/cloakctl/; or: cp -r integrations/hermes/browser-cloakctl ~/.hermes/hermes-agent/plugins/browser/cloakctl then set browser.cloud_provider: cloakctl in config.yaml |
| opencode | merge {"mcp": {"cloakctl": {"type": "local", "command": ["cloakctl-mcp"]}}} into ~/.config/opencode/opencode.json |
| omp | add to ~/.omp/agent.toml: [mcpServers.cloakctl] + command = "cloakctl-mcp" |
| codex | add to ~/.codex/config.toml: [mcp_servers.cloakctl] + command = "cloakctl-mcp" |
| claude code | claude mcp add cloakctl -- cloakctl-mcp |
The hermes plugin is the deep integration: it registers a local
BrowserProvider (persistent per-profile browser, cookie persistence across
agent turns) on the default obscura engine — the keeper's CDP bridge
serves hermes the profile's true session, no Chromium needed (~65MB vs 2GB+).
install.sh wires cloakctl-mcp into hermes' mcp_servers config with a
low RAM floor (CLOAKCTL_MIN_MEM_MB: 150) for small VPS boxes. All other
harnesses consume the MCP server.
cloakctl profiles create linkedin
cloakctl open linkedin # idempotent: re-attaches, never launches twice
cloakctl navigate linkedin --url https://example.com
cloakctl snapshot linkedin # refs e1,e2,... map to interactive elements
cloakctl act linkedin click --ref e3
cloakctl close linkedinEvery verb works over --json for agents (--toon for token-efficient
output; bare cloakctl is a live home view); humans can drop the flag.
Skills carry the how-to-think; workflows carry the how-to-do.
| skill | workflow (wf) |
|
|---|---|---|
| What | agent-owned context + recorded macro | executable automation module |
| Inputs | none — replays fixed steps | typed, validated spec |
| Composes | no — stops at first failure | yes — ctx.call(), cycle + budget fuses |
| Trust | untrusted guidance (may go stale) |
operator-trusted code (same as shell) |
cloakctl wf save title --code 'META = {"inputs": {"url": {"type": "str", "required": True}}}
def run(ctx, inputs):
ctx.navigate(inputs["url"])
return {"title": ctx.exec("document.title")}'
cloakctl wf run title --profile p --set url=https://example.com --timeout 60Run against an open profile. A reusable site trick → skill save; the doing itself compounds (scrape_list → paginate_collect) → wf save --code. Inputs declared "secret": true are redacted from every call tree and log.
Full verb reference (25 verbs, each mirrored as an MCP tool): docs/COMMANDS.md.
cloakctl open linkedin --endpoint wss://cdp.example.com/s/... # or CLOAKCTL_CDP_URL
cloakctl validate linkedin && cloakctl wf run pc --profile linkedin --new-tab # close detaches; browser staysThe VPS holds only refs/meta (idle Python, ~0 persistent RAM). Do the cookie import where the browser lives; put Cloudflare Access on the tunnel — raw CDP is full browser control.
Obscura (default): a second direct connection sees an empty session (per-connection CDP), so external attach goes through the keeper's bridge — a loopback endpoint over the keeper's master connection that IS the profile's real session (same page, same cookies).
attach/status/openreport it; hermes, playwright, and any raw CDP client connect there. Loopback + per-keeper token; not a network service.cloakbrowser (Chromium): native browser-level endpoint, shareable as-is for remote/tunneled setups. An empty
--endpointis refused outright — never a silent local launch.
Measured 2026-09-18 (tests/live_load.py, tests/live_obscura.py), whole process tree:
| probe | latency | memory |
|---|---|---|
| obscura cold launch / exec round-trip | ~0.25 s / ~2 ms | ~65 MB RSS |
| Chromium cold launch / snapshot round-trip | ~0.4 s / ~130 ms | ~1.1–1.5 GB RSS |
| 200 navigate+read cycles (obscura) | ~289 ms/cycle, no leak | 65 → 67 MB |
| 4 live profiles (Chromium, worst case) | — | ~5.0 GB RSS |
Budget ~100 MB per profile (obscura) / ~1.2 GB (Chromium: 2 GB → 1, 4 GB → 2–3, 8 GB → 6). CLOAKCTL_MIN_MEM_MB refuses launches below 400 MB free (hermes' plugin and MCP stanza tune it to 150 for small VPS boxes).
- One page per profile —
tabs newnavigates it (reportednavigated: true); use profiles for isolation. download/historyfail fast — fetch bytes withrun-style JS; useaudittrails. Chromium engine has both.- Uploads need
open <p> --browser-arg=--allow-file-access; non-file inputs are rejected on both engines. - Private/internal IPs are blocked (SSRF guard) unless opened with
--browser-arg=--allow-private-network.
Note
Unattended runs: give every parallel wf run on one profile --new-tab (owned tabs, closed after) — shared-tab parallelism clobbers snapshot refs.
- Single writer — pid + start-time locks; stale locks reclaimed automatically, zombies never read as live.
- Crash-safe teardown — the engine is kernel-tied to its keeper (
PR_SET_PDEATHSIG); SIGKILL leaves no orphans, a wedged page can't hangclose(watchdog + per-call deadlines). - JSON-always — failures are JSON documents with nonzero exit;
doctorexplains what to fix.
| Env | Purpose | Default |
|---|---|---|
CLOAKCTL_ENGINE |
engine for open (obscura, cloakbrowser) |
obscura |
CLOAKCTL_BROWSER |
Chromium binary override (cloakbrowser engine) | auto-detect |
CLOAKCTL_HOME |
state dir (per-suite test isolation) | ~/.cloakctl |
CLOAKCTL_PROBE_URL |
validate target |
LinkedIn feed |
CLOAKCTL_MIN_MEM_MB |
launch RAM floor | 400 |
State lives in ~/.cloakctl/profiles/<name>/ + runtime/ — never edit it directly; every mutation is a CLI verb.
python -m pytest tests/test_core.py tests/test_wf_prod.py tests/test_engines.py tests/test_mcp.py tests/test_install.py # 96 unit, no browser
python tests/live_obscura.py # 29-check obscura matrix (local fixtures, no external network)
python tests/live_bridge.py # 12-check bridge proof: external CDP client == true session
python tests/live_matrix8.py # remote-endpoint matrixPRs: green suite, JSON failures, isolated temp CLOAKCTL_HOME per run. Doctrine deep-dives: docs/SKILLS-VS-WF.md · plans: docs/PLAN.md, docs/WORKFLOWS-PLAN.md. Every doc in docs/ with the question it owns: docs/PAGE-INDEX.md. License: MIT.