unbrowser
Web access for LLM agents. One static binary. No Chrome.
Single-file native headless browser optimized for LLM agents. Runs JavaScript via QuickJS, returns a low-token page summary on every navigate, and gives you stable element refs for click/type/submit. Tens of MB RAM per session, no Chrome dependency.
pip install pyunbrowser
The wheel ships the native binary for your platform — there's nothing else to install.
For transparent bot-wall cookie handoff through local Chrome, install the optional solver extra:
pip install 'pyunbrowser[solver]'
unbrowser cookie-service --headless --profile unbrowser-cookie-service
unbrowser router https://example.com/protected
The solver service binds to 127.0.0.1 by default and returns cookies from the user's local Chrome/unchained session. It does not fabricate challenge tokens. Private/reserved/internal hosts are rejected unless explicitly allowed with --allow-host. Non-loopback binds are rejected unless --allow-remote-bind is passed because /solve is unauthenticated and can return browser cookies. Router refuses non-loopback cookie service URLs by default because it sends target URLs and challenge metadata to that service; use --allow-remote-cookie-service only for a trusted remote solver.
Quick start
from unbrowser import Client
with Client() as ub:
r = ub.navigate("https://news.ycombinator.com")
for s in ub.query(".titleline > a")[:3]:
print(s["text"], s["attrs"]["href"])
Why
| Static / SSR pages | ✅ but token-heavy | ✅ low-token BlockMap | overkill |
SPA-shell sites (with exec_scripts) | ❌ | ⚠️ partial | ✅ |
| Bot-walled (cookie handoff) | ❌ | ✅ | ✅ |
| Run in Lambda / Workers / edge | ✅ | ✅ | ❌ Chrome too big |
| Per-page cost at 100K/day | ~free | ~free | $$$ |
| LLM-shaped output | DIY parse | ✅ inline BlockMap | DIY parse |
What it does
navigate(url) — fetch, parse, return {status, url, bytes, headers, blockmap, challenge}. With exec_scripts=True, runs page JS in QuickJS (bounded by a 30s watchdog so it can't wedge).
query(selector) — CSS query → [{ref, tag, attrs, text, text_chars, text_truncated}]. Refs are stable handles for click/type/submit.
query_debug(selector) — diagnose selector misses with match counts, samples, DOM summary, and selector hints.
extract() — auto-strategy structured data: tries JSON-LD → __NEXT_DATA__ → Nuxt → OpenGraph → microdata → text fallback, returns highest-confidence hit.
extract_table(selector) / table_to_json(selector="table") — normalize HTML tables into headers, rows, and row count.
click(ref) / type(ref, text) / submit(ref) — interaction. GET and application/x-www-form-urlencoded POST forms supported; multipart is not yet supported. Checked checkbox/radio values are serialized.
cookies_set(...) — paste cookies from a real Chrome session to bypass bot detection (Cloudflare, PerimeterX, Datadome). Solve once, replay forever.
Full RPC reference, BlockMap shape, challenge detection, profile system, and architecture: github.com/protostatis/unbrowser.
Honest limits
- Heavy framework SPAs (Ember/React) often don't auto-mount inside QuickJS even with
exec_scripts=True — the watchdog ensures it returns, check density.likely_js_filled to decide whether to escalate.
- No screenshots (out of scope by design).
- Hardest-tier anti-bot (FingerprintJS Pro, Kasada, advanced Akamai BMP) needs the cookie handoff path. The binary detects and labels the challenge for you.
License
Apache-2.0.