unbrowser
Web access for LLM agents. One static binary. No Chrome.
unbrowser is the lightweight open-source browser tier from Unchained: cheap, stateful web access for agents when curl/WebFetch is too dumb and full Chrome is too heavy. When a page needs real Chrome, cookies, extensions, or human-in-the-loop auth, escalate to unchainedsky-cli or Unchained.
Try it hosted: Unchained exposes a public Streamable HTTP MCP endpoint at https://unchainedsky.com/unbrowser-mcp for discovery and smoke tests. Glama also runs a hosted MCP release at glama.ai/mcp/servers/protostatis/unbrowser, and the Smithery page is at smithery.ai/servers/protostatis-dev/unbrowser. These hosted endpoints are shared infrastructure: do not send private cookies, secrets, or authenticated browsing tasks through them. For production workflows, install the local binary below so sessions and cookies stay on your machine.
Install
Python (recommended) — wheel ships the native binary. Requires Python 3.10+:
pipx install pyunbrowser # cleanest on macOS Homebrew / modern Linux (handles PEP 668)
pip install pyunbrowser # in a venv on python3.10+macOS gotcha: the system
/usr/bin/python3is 3.9 and the wheel will reject it with "requires Python >=3.10". Use Homebrew'spython3.13orpipx(which manages its own Python). Ifpip installfails with PEP 668 ("externally-managed-environment"), that's the same issue —pipx install pyunbrowseris the right call.
from unbrowser import Client # note: pip name is pyunbrowser, import is unbrowser
with Client() as ub: # (PyPI's name moderation blocks 'unbrowser';
r = ub.navigate("https://news.ycombinator.com") # py- prefix is the standard workaround)Cargo — binary only, no Python wrapper:
cargo install unbrowser
unbrowser --mcpMCP — add the binary to Claude Code, Claude Desktop, Cursor, Cline, or any MCP host:
{
"mcpServers": {
"unchained": {
"command": "unbrowser",
"args": ["--mcp"]
}
}
}The unchained key is only the client-side alias. Use unbrowser if you want exact naming, or keep unchained as the breadcrumb to the full Unchained browser-agent stack.
Hosted MCP smoke/discovery endpoint — for MCP clients that support Streamable HTTP:
{
"mcpServers": {
"unbrowser-hosted": {
"url": "https://unchainedsky.com/unbrowser-mcp"
}
}
}Use this hosted route to inspect tools or run public-page smoke tests. It is intentionally unauthenticated and SSRF-guarded, and it is not a place to replay private cookies or secrets.
Pre-built tarball — for systems without Python or Rust:
# macOS Apple Silicon
curl -L https://github.com/protostatis/unbrowser/releases/latest/download/unbrowser-aarch64-apple-darwin.tar.gz | tar xz
# macOS Intel
curl -L https://github.com/protostatis/unbrowser/releases/latest/download/unbrowser-x86_64-apple-darwin.tar.gz | tar xz
# Linux x86_64 (glibc 2.31+ / Ubuntu 20.04+)
curl -L https://github.com/protostatis/unbrowser/releases/latest/download/unbrowser-x86_64-unknown-linux-gnu.tar.gz | tar xzFrom source:
cargo build --release # binary at ./target/release/unbrowserSession CLI
For shell-only agents, use a persistent session instead of heredoc JSON-RPC:
unbrowser session start --id demo
unbrowser exec demo navigate https://news.ycombinator.com
unbrowser exec demo query '.titleline > a'
unbrowser exec --pretty demo blockmap
unbrowser session stop demoBare RPC (low-level escape hatch)
echo '{"id":1,"method":"navigate","params":{"url":"https://news.ycombinator.com"}}' | unbrowserThat's the install. Runs anywhere a static binary runs — laptop, Lambda, Cloudflare Workers, edge, embedded.
Open source under Apache 2.0. When the cheap path can't handle a page (heavy SPAs, behavioral bot challenges), escalate to a real browser via unchainedsky-cli (drives your local Chrome via CDP) or the Unchained desktop app.
By the numbers
| This binary | Headless Chrome (Playwright/Puppeteer) | |
|---|---|---|
| Binary size | ~10MB | 250MB+ Chrome download |
| RAM / session | ~50MB | 200–500MB |
| Cold start | ~100ms | ~1s |
| Tokens / page (LLM) | ~500 (BlockMap inline) | tens of thousands of HTML, parsed by you |
| Install steps | cargo build | install Chrome + Node + Playwright + system deps |
| Lambda / Workers / edge | ✅ | ❌ Chrome too big |
| 100K pages/day cost | $0 (your infra) | $$$ Chrome fleet or hosted API |
5–10× lower memory, 25× smaller binary, 10× faster cold start, 70× lower per-page token cost. That's the tradeoff this product makes — defer JS-rendering (Phase 4/5) and pixel rendering (out of scope) in exchange for a footprint that fits in places Chrome doesn't.
Agent-friendly by design
This isn't a Chrome wrapper that an agent uses through a Puppeteer-shaped abstraction. It's a browser whose every output is shaped for LLM consumption:
navigatereturns a BlockMap — ~500 tokens of structured page summary (landmarks, headings, interactives, density signals) right in the response. No follow-up call needed to know what's on the page.- Stable element refs (
e:142) — query, click, type, submit using opaque handles. The LLM never has to scrape the DOM itself. challengefield on every blocked navigate — provider, confidence, and the exact clearance cookie name. The agent reacts intelligently instead of guessing.density.likely_js_filledheuristic — distinguishes "real SSR page" from "SSR shell with JS-filled cells" (the CNBC trap). The agent bails before burning round-trips on a page it can't read.- MCP-native —
unbrowser --mcpexposes the RPC tool surface to any MCP host (Claude Code, Claude Desktop, Cursor, Cline). 4 lines of config, zero glue code. - Real Chrome fingerprint (Chrome 134 JA4 + Akamai H2 hash) so sites don't block you for being a script.
For pages that do need real Chrome (heavy SPAs, JS-challenge bot walls), the binary detects them and accepts cookies via cookies_set — so you solve once in Chrome and replay forever here.
Quick demo — Hacker News top 3
from unbrowser import Client
with Client() as ub:
ub.navigate("https://news.ycombinator.com")
for s in ub.query(".titleline > a")[:3]:
print(s["text"], s["attrs"]["href"])5 lines, no headless browser install. Output is structured JSON, not 35KB of HTML. The Client wrapper handles subprocess lifecycle (atexit reaper so orphans are impossible), JSON-RPC framing, and surfaces real exceptions instead of silent result lookups.
The same demo without the wrapper — useful for languages other than Python or multi-step sessions. The protocol is JSON-RPC over stdin/stdout, one JSON object per line:
import subprocess, json
p = subprocess.Popen(["./target/release/unbrowser"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1)
i = 0
def call(method, **params):
global i; i += 1
p.stdin.write(json.dumps({"id": i, "method": method, "params": params}) + "\n")
p.stdin.flush()
return json.loads(p.stdout.readline())["result"]
call("navigate", url="https://news.ycombinator.com")
for s in call("query", selector=".titleline > a")[:3]:
print(s["text"], s["attrs"]["href"])That's the entire protocol surface. Same shape from any language with subprocess + JSON.
</details>One-shot CLI
For shell-friendly calls, use the convenience subcommand:
unbrowser navigate https://news.ycombinator.com --jsonThat prints one JSON result and exits from any install path (PyPI wheel, Cargo, or release tarball). Use JSON-RPC only when you need a persistent session. Run unbrowser --help for the native CLI surface.
A/B runtime shims
For corpus tests against JS-heavy pages, compare the default stable shims with the opt-in enhanced browser-environment shims:
unbrowser navigate https://example.com --exec-scripts --json
unbrowser navigate https://example.com --exec-scripts --json --shims enhanced
# or for JSON-RPC / MCP sessions:
UNBROWSER_SHIMS=enhanced unbrowserenhanced adds content-positive layout/media/scroll/IndexedDB guesses on top of the stable runtime. It is intentionally opt-in so A/B runs can measure whether more page state materializes without changing the baseline.
Script evaluation is still bounded by UNBROWSER_SCRIPT_EVAL_BUDGET_MS (default 5000); navigate results report scripts.budget_exhausted and scripts.budget_skipped when the budget stops further script execution. The outer RPC watchdog (UNBROWSER_TIMEOUT_MS, default 30000) still wins if it is lower than the script budget.
For a JSONL corpus sweep:
python3 scripts/shim_ab.py --url https://nextjs.org/docs --url https://www.npmjs.com/package/playwrightSPA tier — what works, what doesn't
Empirical, not aspirational. Latest matrix: 28/30 on tested categories.
| Page tier | Coverage | What to expect |
|---|---|---|
| Static + SSR (Wikipedia, MDN, news, docs, GitHub repo browsing, search engines, archive.org) | ✅ excellent | sub-second navigate; full BlockMap; all selectors work; ~hundreds of tokens vs ~tens of KB raw |
| SSR + light hydration (Next.js docs, marketing pages, react.dev's static content) | ✅ usable | reads SSR'd content fine; hydration adds nothing but doesn't break either |
| Bot-walled with cookie handoff (Zillow, Cloudflare-protected sites) | ✅ via cookies_set | solve once in Chrome, replay forever; challenge.provider field tells the agent which vendor |
| Module-loader SPAs (Ember, AMD apps like crates.io) | ⚠️ partial with exec_scripts: true | bundles fetch + execute, modules register, but framework auto-mount needs case-by-case shimming |
| Heavy React/Vue bundles (react.dev runtime, large dashboard apps) | ⚠️ bounded — won't hang, won't render | with exec_scripts: true the navigate completes inside the 30s wall-clock budget (5s for the script-eval phase, the rest for settle); rendered DOM may not materialize. Tune via UNBROWSER_TIMEOUT_MS |
| Apps requiring Workers / Canvas / IndexedDB / WebGL | ❌ out of scope by design | use the cookie-handoff path with real Chrome via unchainedsky-cli (CDP) or the Unchained desktop app |
| Hardest-tier anti-bot (PerimeterX with behavioral, Kasada, Akamai BMP advanced) | ❌ even cookie handoff is fragile | real Chrome via CDP is the right tier |
Vs the alternatives:
| This | curl | Playwright / headless Chrome | |
|---|---|---|---|
| Static / SSR pages | ✅ | ✅ but token-heavy | overkill |
| SPA-shell sites | ⚠️ partial via exec_scripts | ❌ | ✅ |
| Bot-walled (with cookie handoff) | ✅ | ❌ | ✅ |
| Run in Lambda / Workers / edge | ✅ | ✅ | ❌ Chrome too big |
| Per-page cost at 100K/day | ~free | ~free | $$$ |
| LLM-shaped output | ✅ BlockMap inline | DIY parse | DIY parse |
Verified against (working)
Concrete sites tested with measured times. Cold-start to extracted-result.
| Category |
…