Quickstart¶
This assumes the proxy is running (see Install) and
curl localhost:9223/health returns {"ok": true, ...}.
1. Point your existing tools at 9222¶
Nothing changes. Whatever you already run against port 9222 keeps working, and now only sees its own tabs:
What each kind of client gets:
- Browser-level websocket (Playwright, Puppeteer,
chrome-devtools-mcp, anything that readswebSocketDebuggerUrlfrom/json/version): one owner for as long as the socket stays open. If it asks for targets before creating any, it is given one freshabout:blanktab so "use the first page" works. All its tabs close when it disconnects. - Legacy
/jsonclient (GET /json, then connect to a page socket):/jsonlists exactly one unclaimed tab made for it. That tab is reserved for 10 seconds, and only one page socket may attach. It closes when that socket closes.
2. Or lease a tab from Python¶
Copy broker_client.py next to your agent (it needs websocket-client):
from broker_client import lease
with lease("my-agent", url="https://mail.google.com", purpose="inbox sweep") as tab:
print(tab.eval("document.title"))
tab.goto("https://example.com")
tab.send("Page.reload")
lease(owner, url=None, purpose=None, hidden=True, ttl=300)opens a background tab and yields aTab.- The client renews the lease every 60 seconds while the block runs, retrying a failed renew up to 5 times with backoff, and stops only when the broker answers 404.
- On exit, including on an exception, it closes the socket and releases the lease, which closes the tab. If the release fails, the server reaps the tab when the TTL runs out.
Tab methods:
| Method | What it does |
|---|---|
send(method, **params) |
Send any CDP command and wait for its reply. Raises RuntimeError on a CDP error. |
goto(url, wait=3) |
Page.enable, Page.navigate, then sleeps wait seconds. |
eval(expr) |
Runtime.evaluate with returnByValue and awaitPromise; returns the value. |
close() |
Close the websocket. |
3. Or use the REST API directly¶
curl -s -X POST localhost:9223/lease \
-H 'Content-Type: application/json' \
-d '{"owner":"my-agent","url":"https://example.com","ttl":300}'
# {"lease_id": "...", "target_id": "...", "ws_url": "ws://127.0.0.1:9222/devtools/page/...", "expires_in": 300}
Connect to ws_url with any CDP client. Renew before the TTL runs out with
POST /renew {"lease_id": "..."} and finish with POST /release {"lease_id": "..."}.
Full reference: Configuration.
4. Showing a page to the human¶
Agents should not run open -a "Brave Browser" <url>. That goes straight to macOS,
around the proxy, so two agents doing it throw the window back and forth. Use the stage:
| Exit code | Meaning |
|---|---|
0 |
Shown. |
2 |
Another agent has the screen. Print the link and move on; don't force it. |
1 |
Error (no URL given, or the fallback open -a failed). |
From Python:
from broker_client import show, stage
show("https://example.com", owner="my-agent", purpose="report")
print(stage()) # who has the screen right now
show() raises RuntimeError("screen busy: ...") if the screen stayed busy past wait.
To make this binding for Claude Code, see the example PreToolUse hook in
Configuration.