CLI Browser
September 2, 2026 · View on GitHub
Drive one real browser from the shell — call browser functions directly, or hand a task to the AI agent.
Quick Start (60 seconds)
co browser go_to news.ycombinator.com # opens a browser, navigates
co browser get_current_url # → https://news.ycombinator.com/
co browser take_screenshot /tmp/shot.png # saves a PNG, prints the path
co browser close # done
The browser stays open between commands. Each co browser ... call drives the same window — your navigation, cookies, and logged-in session persist until you close.
Choosing the browser engine
ConnectOnion 1.8 resolves the engine once when the browser daemon starts:
co browser --engine auto go_to example.com # default
co browser --engine system go_to example.com # always Patchright + system Chrome
co browser --engine onion go_to example.com # strict paid Onion Browser
The paid path requires Onionwright 0.0.13 or newer. Install or upgrade the real private wheel explicitly:
co browser install-onion
This command uses the current ConnectOnion login, verifies the release feed with ConnectOnion's pinned Ed25519 public key, verifies the wheel checksum from that signed manifest, and only then invokes this Python interpreter's pip. It does not start the browser daemon or a paid session, and downloading/installing the client costs $0. The real wheel comes from OpenOnion's authenticated artifact endpoint, not the public PyPI placeholder.
| mode | behavior |
|---|---|
auto | Run Onionwright's non-billing compatibility/artifact preflight. Use the exact verified Onion artifact when ready; otherwise use system Chrome and report a typed fallback reason. |
system | Return before importing Onionwright, reading paid credentials, calling oo-api, downloading an artifact, or creating a paid session. Cost: $0 browser runtime. |
onion | Require the compatible Onion artifact and enough balance. Any preflight failure is returned as a typed error; there is no silent system fallback. |
Artifact checking and download do not charge. A paid session starts only after
the complete artifact is locally ready, then prepays $0.025 for one 15-minute
interval. Renewal occurs before the signed deadline. If renewal fails, that
exact Onion process stops at paid_until; it is never changed into system
Chrome mid-session.
The daemon is pinned to its chosen engine. Close it before changing modes:
co browser close
co browser --engine system go_to example.com
System Chrome and Onion Browser use separate persistent profiles. Cookies and
fingerprint state are not silently copied between them. co browser status
shows requested/resolved engine, typed reason, and exact artifact when paid;
it never prints tokens, licence bytes, or paid-cache paths.
Why Use This
Two ways to use a browser from the CLI, and you pick per command:
- Direct function call —
co browser go_to x.com. Deterministic and instant, with no LLM charge; browser runtime cost follows the selected engine. Great for scripting and exact steps you already know. - Natural language —
co browser do "find the cheapest flight". The AI agent figures out the steps. Great when you don't want to spell them out.
Both drive the same live browser, so you can mix them: script the boring parts, let the agent handle the hard part.
co browser go_to myapp.com/login
co browser do "log me in and open the billing page" # agent takes over the same window
co browser take_screenshot /tmp/billing.png # back to a direct call
How It Works
The first co browser command starts a small background daemon that owns one browser. Every later command connects to it over a local socket and drives that same browser. The daemon lives exactly as long as the browser:
co browser go_to x.com ──► starts daemon ──► opens browser ─┐
co browser click "Login" ──────────────────► same browser │ state persists
co browser screenshot ──────────────────► same browser │
co browser close ──► browser closes ──► daemon exits ─┘
You never manage the daemon directly — the first command starts it, and close (or closing the window) stops it. There is no separate "start" step.
How a command is dispatched
The first word is compared against the browser's function names:
| You type | What happens |
|---|---|
co browser go_to x.com | go_to is a function → runs it directly |
co browser do "..." | do → hands the instruction to the AI agent |
co browser frobnicate | matches nothing → unknown command: frobnicate (exit 1) |
Quote natural-language instructions:
co browser do "click the blue button". A bare word that happens to be a function name (likeclick) is treated as a direct call, not language.
Discovering Functions
The CLI describes itself — run help to list every callable function with its arguments and a one-line summary (no browser is launched):
co browser help
Functions:
go_to(url) — Navigate to a URL.
take_screenshot(path=None, full_page=False) — Take a screenshot of the current page...
click(description) — Click on an element using natural language description.
get_links_from_page(domain_filter='') — Extract all unique links from the current page...
...
This is the fastest way — for a person or an AI agent — to find the exact function name and arguments before calling it.
Common Functions
Any function listed by co browser help is callable. The ones you'll reach for most:
co browser go_to <url> # navigate
co browser get_current_url # print the current URL
co browser get_text # print visible page text
co browser take_screenshot /tmp/shot.png [--full-page]
co browser click "<description or selector>"
co browser type_text_by_selector <css> "<text>"
co browser fill_text_by_selector <css> --stdin < secret.txt # replace controlled input; secret stays out of argv
co browser get_focused_element # bounded JSON; password values are redacted
co browser keyboard_press Meta+a # refused unless focus is editable
co browser get_links_from_page # one link per line
co browser scroll # scroll the main content
co browser close # close browser, stop daemon
Arguments are plain strings; flags like --full-page and --index=2 map to the function's parameters. For fill_text_by_selector, type_text_by_selector, and keyboard_type, a final --stdin reads the text from redirected standard input so passwords and one-run codes do not appear in process arguments. Prefer fill_text_by_selector when replacing a controlled framework input; use type_text_by_selector when appending human-shaped keystrokes is required.
Before replacing focused text with a keyboard shortcut, inspect the target:
co browser get_focused_element
co browser keyboard_press Meta+a # macOS
co browser keyboard_press Control+a # Windows/Linux
co browser keyboard_press Backspace
get_focused_element follows focus into open shadow roots and reports whether
the target is editable. Its value preview is bounded, and password values are
always redacted. keyboard_press refuses select-all, Backspace, and Delete when
focus is outside an editable input, textarea, or contenteditable element. For a
deliberate page-level shortcut, acknowledge the risk explicitly with
--allow-non-editable.
Focus inspection is scoped to the top-level document and open shadow roots. If focus is inside an iframe, the result describes the iframe itself and treats it as non-editable; closed shadow roots cannot be inspected. These cases fail safe: target the field by selector, or use the explicit override only after verifying the frame and intended page-level action.
Use absolute paths for files. The daemon resolves relative paths against its own working directory (where it was first started), not the directory you run each command from.
take_screenshot /tmp/shot.pngis predictable; a bareshot.pnglands in the daemon's.tmp/folder.
Screenshots
take_screenshot writes a PNG and prints where it saved — not the image data:
$ co browser take_screenshot /tmp/shot.png
Screenshot saved to: /tmp/shot.png
Omit the path and it auto-names the file under the daemon's .tmp/ folder:
$ co browser take_screenshot
Screenshot saved to: /Users/you/project/.tmp/step_20260630_142927.png
Add --full-page to capture the entire scrollable height instead of just the viewport.
Why a path, not the image? The underlying
take_screenshot()function returns a base64 data URL — that's what the AI agent "sees" when it drives the browser withdo. A direct CLI call deliberately prints the file path instead, soco browser take_screenshotnever floods your terminal with a screenful of base64. Open or pipe the saved file when you want the actual image.
Scripting
Output is clean stdout, errors go to stderr, and the exit code is 0 on success / 1 on failure — so commands compose like any Unix tool:
# Capture a value
url=$(co browser get_current_url)
# Pipe list output (one item per line)
co browser get_links_from_page | grep github | wc -l
# Fail-fast in a script
co browser go_to "$DEPLOY_URL" && co browser take_screenshot /tmp/deployed.png
Sharing the Browser With Other Agents
One machine, one browser, often several agents. They stay out of each other's way through named tabs — and by saying how long they expect to need one.
co browser tab open scrape --who alice --for "scrape pricing" --needs 10m
co browser -t scrape go_to example.com/pricing # -t on EVERY command
co browser -t scrape get_text
co browser tab close scrape # release when done
--needs takes 30s, 10m, or 2h. It is not a lock — it is the estimate
other agents read before touching your tab:
co browser tab ls
Tabs (2):
[scrape] https://example.com/pricing who=alice purpose='scrape pricing' open 3m
last: "get_text" · 12s ago
owner expects to finish by 14:20 (7m left) — leave it alone until then
[stale] https://... who=bob purpose='check stock' open 2h
owner expected to finish by 12:30 (1h ago) — free for another agent to close
Inside the window, leave it alone — open your own tab instead. Once it has passed, the tab is free, and closing it is a courtesy: an estimate that ran out with the tab still open means that agent crashed, not that it is still working.
A tab opened without --needs frees up after ~2 minutes of silence, which is
wrong whenever you are waiting on a slow page or a human. Say the number.
Named tabs are also the concurrency boundary. The daemon owns one asyncio browser
runtime: two operations aimed at scrape queue behind each other, while scrape
and inbox may run at the same time. A registry lock decides claim races before
either operation touches a page, so concurrency never means two agents silently
sharing one tab.
Set CO_WHO so your commands carry your identity:
export CO_WHO=alice
Headless vs GUI
By default the browser is visible (a real Chrome window you can watch). Add --headless for scripts/CI:
co browser --headless go_to example.com # no window
co browser go_to example.com # visible window (default)
The mode is fixed when the daemon starts (the first command). To switch modes, co browser close first, then start again with the mode you want.
Natural Language Agent
do runs the full AI browser agent on the live browser and prints its final answer:
co browser do "search for wireless headphones and list the top 3 prices"
This path uses managed keys — run co auth once if you see an authentication message.
Installation
None needed (1.2.1+). The Patchright library ships with connectonion, and the first page-driving command auto-installs a browser when none exists — a one-time download, announced in your terminal, into your per-user directory (no admin rights). If a desktop Google Chrome is installed at the standard location, it is detected and used instead, with zero downloads.
Manual fallback (older versions, airgapped machines, or a failed auto-install):
python -m patchright install chromium # per-user, never needs admin
python -m patchright install chrome # branded Chrome: best stealth, system installer
Sessions & Profile
- One async browser runtime per machine, backed by a persistent profile at
~/.co/browser_profile/— so logins survive restarts. - The daemon endpoint: a Unix socket under
$XDG_RUNTIME_DIR/co/browser.sockon macOS/Linux, a per-user named pipe on Windows (native, 1.2.1+ — no WSL). Override with$CO_BROWSER_SOCK. - Client work is bounded: 1 MiB request cap, 120-second read/reply deadlines, 32 admitted connections, and eight blocking transport workers on Windows.
- On Windows,
co browser closereturns only after the serving daemon exits, so an immediate next command can safely start a fresh daemon. - For an isolated automation run, set
$CO_BROWSER_PROFILE_DIRto a dedicated absolute directory and$CO_BROWSER_SOCKto a dedicated socket. Keep the real$HOME; replacing it can break OS-backed browser behavior and credentials.
Error Messages
Errors print to stderr and exit with code 1. Each one tells you the next step — handy when an AI agent is driving the CLI and needs to self-correct.
Unknown function
$ co browser frobnicate
unknown command: frobnicate
Run 'co browser help' to list functions, or 'co browser do "<instruction>"' for natural language.
The first word didn't match any browser function. List them with co browser help, or use do to describe the task in plain English.
Wrong arguments
$ co browser go_to
TypeError: BrowserAutomation.go_to() missing 1 required positional argument: 'url'
usage: go_to(url)
The function exists but the arguments don't fit. The usage: line shows the exact signature — pass the missing argument: co browser go_to example.com.
Authentication required (only for do)
$ co browser do "find the price"
Browser agent requires authentication. Run: co auth
The natural-language agent uses managed keys. Run co auth once. Direct function calls don't need this.
Patchright not installed
Browser tools not installed. Run: pip install patchright && patchright install chrome
Troubleshooting
# Nothing happens / stuck browser → close and start fresh
co browser close
# See what the agent/daemon is doing
cat ~/.co/browser.log
# Authentication needed (only for `do`)
co auth
See Also
co auth— managed keys for thedoagent- Browser tools library —
BrowserAutomationused in your own agents - Templates — scaffold a project whose agent drives this CLI