The headless browser built in pure Rust for AI agents.
Not a Chromium fork. Not a C++ wrapper. A browser engine written from scratch in Rust, designed from day one for automation, web scraping, and AI-driven workflows.
| 24 MB Single static binary |
~50 ms Cold start time |
~8 MB Base memory |
554 tests Full coverage |
Rust-first C toolchain for TLS only |
| OxiBrowser | Headless Chrome |
|---|---|
| 24 MB binary | ~400 MB install |
| ~8 MB RAM base | ~200 MB RAM base |
| ~50 ms startup | ~800 ms startup |
| Pure Rust (boa) | C++ (V8) |
| MIT | BSD / ToS |
You're building AI agents that need to browse the web. You don't need a full browser with GPU rendering, audio output, and extension support. You need something fast, small, and programmable.
OxiBrowser is built for exactly that use case:
- 🤖 AI-Agent First — CLI designed for agents:
--jsonoutput,describefor schema,skillfor prompts,sessionfor multi-step - ⚡ Blazing Fast — Cold starts in ~50ms, no Chromium overhead, no Node.js required
- 🦀 Rust-First —
boa_engine(JS, no V8),html5ever(HTML) are pure Rust. TLS usesbtls(BoringSSL C binding) for stealth fingerprint emulation. Single static binary. - 🔌 CDP Compatible — Puppeteer, Playwright, and any Chrome DevTools Protocol client works out of the box
- 🛡️ Secure by Default — SSRF protection with CIDR blocking,
robots.txtrespect, no sandbox escape surface - 📦 Tiny Footprint — 24 MB binary, ~8 MB base memory. Run 100 instances without breaking a sweat
This release closes most of the gap between "HTML fetcher" and "real headless browser" — events now behave like a browser, innerHTML works for SPA-style DOM injection, and fetch returns a spec-compliant Response with arrayBuffer().
Events & DOM
- Event constructors honor the init dict —
new MouseEvent('click', { clientX, clientY, ctrlKey, ... })now actually carries those fields. CoversMouseEvent,KeyboardEvent,FocusEvent,Event, and a newDragEvent. dispatchEventsetsevent.target/event.currentTargetand returns!defaultPrevented.preventDefault/stopPropagation/stopImmediatePropagationwork on every event.- Event bubbling walks the parent chain. Listeners are stored in a thread-local registry keyed by
nodeId, so they survive across element-object re-queries (the bug that madeparent.addEventListenerinvisible tochild.dispatchEvent). requestAnimationFrame/cancelAnimationFramenow schedule properly with a 16 ms deadline and pass aDOMHighResTimeStampto the callback.innerTextgetter and standaloneperformanceglobal (window.performance === performance).
HTML & innerHTML
innerHTMLsetter parses the fragment viahtml5everand inserts child nodes into the snapshot.outerHTMLgetter serializes the node back. A newdom_serializermodule handles the round-trip with proper void-element / attribute-escape handling (12 unit tests).
Network
Response.text()/json()/arrayBuffer()all return spec-shaped Promises that resolve to the actual response body.fetchoptionsheaders(content-type, accept, authorization, user-agent, cookie) are now forwarded.- SSRF filter is now scheme-aware: only
http/httpsgo through DNS/host checks.about:blankis supported (Puppeteer/Playwright's default target URL now works).
CDP
Input.dispatchMouseEventemits a real sequence:mousePressed→mousedown;mouseReleased→mouseup+click;mouseMoved→mousemove.Input.dispatchDragEventis wired to aDragEventon the element at the point.
cargo install oxibrowser$ oxibrowser fetch https://example.com
Example Domain
# Example Domain
This domain is for use in documentation examples...
[Learn more](https://iana.org/domains/example)$ oxibrowser fetch https://example.com --json
{"ok":true,"data":{"url":"https://example.com/","title":"Example Domain","status":200,"markdown":"..."},"meta":{"elapsed_ms":152}}$ oxibrowser extract https://example.com --links --json
{"ok":true,"data":{"links":["https://iana.org/domains/example"],"title":"Example Domain"}}$ oxibrowser session
new
{"ok":true,"data":{"tab_id":"t1"}}
goto t1 https://example.com
{"ok":true,"data":{"status":200,"title":"Example Domain"}}
eval t1 document.title
{"ok":true,"data":{"value":"Example Domain"}}
close t1
{"ok":true,"data":{"closed":"t1"}}
exit
{"ok":true,"data":{"exit":true}}oxibrowser serve --port 9222import puppeteer from 'puppeteer-core';
const browser = await puppeteer.connect({
browserWSEndpoint: 'ws://127.0.0.1:9222',
});
const page = await browser.newPage();
await page.goto('https://news.ycombinator.com');
console.log(await page.title());
await browser.close();oxibrowser <COMMAND>
COMMANDS:
fetch Fetch a URL and return content (markdown default)
extract Extract structured data (links, text, elements)
run Run a YAML automation script
session Interactive stdin/stdout JSON REPL (22 commands)
serve Start CDP WebSocket server
search Web / GitHub / GitHub-issues search (no browser needed)
describe Print CLI schema as JSON (for agents)
skill Print agent skill guide
version Print version information
# Human-readable (markdown, default)
oxibrowser fetch https://example.com
# Agent mode
oxibrowser fetch https://example.com --json
# Click then read
oxibrowser fetch https://example.com --click button --wait .result --json
# Quick page summary
oxibrowser fetch https://example.com --summary --json
# Run JS
oxibrowser fetch https://example.com --eval "document.title" --json
# Limit response size
oxibrowser fetch https://example.com --max-bytes 8000 --json
# Select specific fields
oxibrowser fetch https://example.com --fields url,title,status --json# Get all links
oxibrowser extract https://example.com --links --json
# Extract elements by CSS selector
oxibrowser extract https://example.com --selector "a" --all --attrs text,href --json
# Title + full text
oxibrowser extract https://example.com --title --text --jsonoxibrowser session # Start REPL
# 22 commands:
new, goto, back, forward, reload, click, fill, press, type,
select, check, uncheck, scroll, eval, extract, content,
screenshot, wait, close, close --all, list, help, exit# Compact (~200 tokens)
oxibrowser describe --compact
# Full command details
oxibrowser describe fetch
oxibrowser describe session# Web search (DuckDuckGo)
oxibrowser search "rust async" --engine ddg --max-results 5 --json
# GitHub search
oxibrowser search "memory pool" --source github --json
# GitHub issues for a specific repo
oxibrowser search "panic on shutdown" --source github-issues --repo a7garden/oxibrowser --jsonname: example
steps:
- step_type: goto
data:
goto: https://example.com
- step_type: content
data:
format: markdownoxibrowser run script.yamlAll --json responses follow the same schema:
{
"ok": true,
"data": { ... },
"meta": { "elapsed_ms": 152 }
}On error:
{
"ok": false,
"error": "URL scheme must be http or https",
"error_code": "INVALID_URL"
}Exit codes: 0=success, 1=runtime, 2=input validation, 3=timeout, 4=network
┌──────────────────────────────────────────────────────┐
│ Puppeteer / Playwright / Rust CDP │
└────────────────────────┬─────────────────────────────┘
│ CDP WebSocket
▼
┌──────────────────────────────────────────────────────┐
│ CDP Server (10 domains) │
│ Browser · DOM · Fetch · Input · Network │
│ OXI · Page · Runtime · Target │
├──────────────────────────────────────────────────────┤
│ Browser → Session → Page → Frame │
├──────────┬──────────┬──────────────┬─────────────────┤
│ WebAPI │ Network │ JS Runtime │ CSS Rendering │
│ DOM │ HTTP │ boa_engine │ PNG screenshot │
│ Tree │ Cookies │ ES2024+ │ ASCII/Unicode │
│ Storage │ SSRF │ persistent │ text→image │
├──────────┴──────────┴──────────────┴─────────────────┤
│ html5ever · encoding_rs · reqwest · image · boa │
└──────────────────────────────────────────────────────┘
| Crate | Lines | Purpose |
|---|---|---|
oxibrowser |
4,242 | Binary + CLI (8 subcommands, session REPL, agent features) |
oxibrowser-core |
19,794 | Browser engine: Session, Page, Frame, JS Runtime |
oxibrowser-cdp |
4,583 | CDP WebSocket server with 10 domain handlers |
oxibrowser-webapi |
1,587 | DOM tree, CSS selectors, Markdown conversion |
| Total | 30,206 |
Designed for AI agent workflows — no daemon, no socket, single binary:
| Feature | Description |
|---|---|
--json |
Machine-readable output (opt-in, human by default) |
--max-bytes N |
Truncate response to N bytes |
--fields a,b,c |
Select specific output fields |
--summary |
Quick page metadata (title, links, headings) |
describe |
CLI schema as JSON for agent introspection |
skill |
Agent skill guide for prompt injection |
session |
Stdin/stdout JSON REPL with 22 commands |
| Exit codes | 0=success, 1=runtime, 2=input, 3=timeout, 4=network |
Powered by boa_engine — pure Rust, no V8 dependency:
| Web API | Status |
|---|---|
document.querySelector / querySelectorAll |
✅ Full |
document.createElement / createTextNode |
✅ Full |
element.appendChild / removeChild / insertBefore |
✅ Full |
element.getAttribute / setAttribute / removeAttribute |
✅ Full |
element.cloneNode / remove() |
✅ Full |
element.style (CSSStyleDeclaration) |
✅ Property accessor |
element.classList (DOMTokenList) |
✅ Property accessor |
element.textContent / innerHTML |
✅ Read/Write |
element.addEventListener / dispatchEvent |
✅ Full |
element.click() |
✅ With event handlers |
fetch() |
✅ Full (channel bridge) |
XMLHttpRequest |
✅ Full with callbacks |
localStorage |
✅ Persistent |
MutationObserver |
✅ observe/disconnect/takeRecords |
setTimeout / setInterval |
✅ TokioJobQueue |
console.log/warn/error |
✅ With formatting |
URL / URLSearchParams |
✅ Full |
crypto.getRandomValues |
✅ Pseudo-random |
TextEncoder / TextDecoder |
✅ UTF-8 |
atob / btoa |
✅ Base64 |
requestAnimationFrame |
✅ Polyfill |
10 domain handlers — Puppeteer and Playwright compatible:
| Domain | Key Methods |
|---|---|
| Browser | getVersion, close |
| DOM | getDocument, describeNode, querySelector, querySelectorAll |
| Fetch | enable/disable, continueRequest, failRequest, fulfillRequest, getResponseBody |
| Input | dispatchKeyEvent, dispatchMouseEvent, insertText |
| Network | enable/disable, setExtraHTTPHeaders, getResponseBody |
| OXI 🤖 | getMarkdown, getPageInfo — AI-native extensions |
| Page | navigate, captureScreenshot, getFrameTree, getTitle |
| Runtime | evaluate, callFunctionOn, enable, consoleAPICalled |
| Target | getTargets, attachToTarget, detachFromTarget |
import websockets, json, asyncio
async def ai_scrape():
ws = await websockets.connect('ws://localhost:9222/ws')
await ws.send(json.dumps({
"id": 1, "method": "Page.navigate",
"params": {"url": "https://news.ycombinator.com"}
}))
await asyncio.sleep(2)
# Clean markdown — perfect for LLM ingestion
await ws.send(json.dumps({"id": 2, "method": "OXI.getMarkdown"}))
resp = json.loads(await ws.recv())
print(resp['result']['markdown'])| Feature | Description |
|---|---|
| HTTP Client | reqwest with cookie persistence, redirect following |
| Cookie Jar | Domain-scoped cookie storage with Set-Cookie parsing |
| SSRF Protection | CIDR blocking for private network ranges |
| robots.txt | RFC 9309 compliant parser, --obey-robots flag |
| Network Interception | Pause, modify, or block any request via Fetch domain |
| Custom Headers | Per-session and per-request header injection |
| Charset Detection | encoding_rs for automatic charset detection and conversion |
- ASCII/Unicode text output — Render DOM to readable text with proper indentation
- Markdown conversion — Full HTML→Markdown with heading, link, and list support
- PNG screenshots — Built-in 8×16 bitmap font, renders text content as images
- No external dependencies — Font data embedded in binary
# Run all tests
cargo test --workspace
# CLI integration tests (fast, no network)
cargo test -p oxibrowser --test cli
# E2E CDP tests
cargo test -p oxibrowser-cdp
# Integration tests (real websites, requires internet)
cargo test --workspace -- --ignoreduse oxibrowser_core::Browser;
use oxibrowser_core::config::BrowserConfig;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let browser = Browser::new(BrowserConfig::default()).await?;
let session = browser.new_session().await?;
session.navigate("https://example.com").await?;
let title = session.evaluate("document.title").await?;
println!("Title: {:?}", title);
Ok(())
)
}[dependencies]
oxibrowser-core = "0.11"
# Or the CDP server:
oxibrowser-cdp = "0.11"const client = await page.target().createCDPSession();
await client.send('Fetch.enable', {
patterns: [{ urlPattern: '*ads*' }]
});
client.on('Fetch.requestPaused', async ({ requestId }) => {
await client.send('Fetch.failRequest', {
requestId,
reason: 'BlockedByClient'
});
});See CONTRIBUTING.md for full guidelines.
git clone https://github.com/a7garden/oxibrowser.git
cd oxibrowser
cargo build
cargo test --workspace
cargo clippy --workspace -- -D warningsOxiBrowser is licensed under the MIT License.
- boa_engine — Pure Rust JavaScript engine (ES2024+)
- html5ever — HTML parser from the Servo project
- reqwest — Ergonomic HTTP client for Rust
- tokio — Async runtime powering the entire networking stack
Made with 🦀 in Rust