I did some (really shallow) research and there is lightpanda that seams a bit better solution to search the web from some agent than some wrapper around the Chrome Developer Tools.
Browser automation & web crawling for AI agents. Written in Zig. Zero Node.js.
CDP automation Β· A11y snapshots Β· HAR recording Β· Standalone fetcher Β· Interactive terminal browser Β· Agentic CLI Β· Security testing
Quick Start Β· Benchmarks Β· kuri-agent Β· Security Testing Β· API Β· Changelog
Why teams switch to Kuri: 464 KB binary, ~3 ms cold start. On Google Flights, a full agent loop (goβsnapβclickβsnapβeval) costs 4,110 tokens vs 4,880 for agent-browser β 16% less per cycle, compounding across multi-step tasks.
Most browser tooling was built for QA engineers. Kuri is built for agent loops: read the page, keep token cost low, act on stable refs, and move on.
SIN β TPESame Chrome session, measured with tiktoken cl100k_base. Run ./bench/token_benchmark.sh to reproduce.
| Tool / Mode | Bytes | Tokens | vs kuri |
Note |
|---|---|---|---|---|
kuri snap (compact) |
13,479 | 4,328 | baseline | |
kuri snap --interactive |
7,024 | 1,927 | 0.4x | Best for agent loops |
kuri snap --json |
102,124 | 31,280 | 7.2x | Old default |
agent-browser snapshot |
17,103 | 4,641 | 1.1x | |
agent-browser snapshot -i |
8,704 | 2,425 | 0.6x | |
lightpanda semantic_tree |
67,830 | 26,244 | 6.1x | β no JS β raw DOM |
lightpanda semantic_tree_text |
1,909 | 507 | 0.1x | β no JS β empty shell |
go β snap β click β snap β eval| Tool | Tokens per cycle |
|---|---|
| kuri-agent | 4,110 |
| agent-browser | 4,880 |
kuri saves 16% tokens per workflow cycle β compounding across multi-step tasks.
Action responses are flat JSON ({"ok":true}) instead of nested CDP, which adds up:
click = 9 tokens, back = 5 tokens, scroll = 5 tokens.
Why lightpanda scores low: Lightpanda can't execute JS-heavy SPAs. Google Flights renders via client-side
fetch()β lightpanda returns a 507-token empty nav shell with zero flight data. The low token count is a failed render, not efficiency.
Measured on Apple M3 Pro, macOS 15.3. kuri built with -Doptimize=ReleaseFast. agent-browser v0.20.0.
agent-browser kuri delta
(v0.20) (v0.2)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CLI binary 6.0 MB 464 KB 13Γ smaller
Cold start (--version) 3.4 ms 3.0 ms ~same
Install (npm) 33 MB 3.3 MB (3 bins) 10Γ smaller
Commands 140+ 40+ endpoints different focus
Standalone fetcher β β
kuri-fetch no Chrome needed
Terminal browser β β
kuri-browse interactive REPL
JS engine (no Chrome) β β
QuickJS SSR-style DOM
HTTP API server β (CLI only) β
kuri thread-per-conn
agent-browser exposes a broader browser-control surface. Kuri is intentionally narrower: a lightweight HTTP API and CLI stack optimized for agent integration, token economy, and deployment simplicity.
Every browser automation tool drags in Playwright (~300 MB), a Node.js runtime, and a cascade of npm dependencies. Your AI agent just wants to read a page, click a button, and move on. Kuri is a single Zig binary. Four modes, zero runtime:
kuri β CDP server (Chrome automation, a11y snapshots, HAR)
kuri-fetch β standalone fetcher (no Chrome, QuickJS for JS, ~2 MB)
kuri-browse β interactive terminal browser (navigate, follow links, search)
kuri-agent β agentic CLI (scriptable Chrome automation + security testing)
curl -fsSL https://raw.githubusercontent.com/justrach/kuri/main/install.sh | sh
Detects your platform, downloads the right binary, installs to ~/.local/bin.
macOS binaries are notarized β no Gatekeeper prompt.
bun install -g kuri-agent
# or: npm install -g kuri-agent
Downloads the correct native binary for your platform at install time.
Download the tarball for your platform from GitHub Releases and unpack it to your $PATH.
Requires Zig β₯ 0.15.0.
git clone https://github.com/justrach/kuri.git
cd kuri
zig build -Doptimize=ReleaseFast
# Binaries in zig-out/bin/: kuri kuri-agent kuri-fetch kuri-browse
Requirements: Zig β₯ 0.15.1 Β· Chrome/Chromium (for CDP mode)
git clone https://github.com/justrach/kuri.git
cd kuri
zig build # build everything
zig build test # run 230+ tests
# CDP mode β launches Chrome automatically
./zig-out/bin/kuri
# Standalone mode β no Chrome needed
./zig-out/bin/kuri-fetch https://example.com
# Interactive browser β browse from your terminal
./zig-out/bin/kuri-browse https://example.com
# start the server; if CDP_URL is unset, kuri launches managed Chrome for you
./zig-out/bin/kuri
# discover tabs from that managed browser
curl -s http://127.0.0.1:8080/discover
# inspect the discovered tab list
curl -s http://127.0.0.1:8080/tabs
If you already have Chrome running with remote debugging, set CDP_URL to either the WebSocket or HTTP endpoint:
CDP_URL=ws://127.0.0.1:9222/devtools/browser/... ./zig-out/bin/kuri
# or
CDP_URL=http://127.0.0.1:9222 ./zig-out/bin/kuri
# 1. Discover Chrome tabs
curl -s http://localhost:8080/discover
# β {"discovered":1,"total_tabs":1}
# 2. Get tab ID
curl -s http://localhost:8080/tabs
# β [{"id":"ABC123","url":"chrome://newtab/","title":"New Tab"}]
# 3. Navigate
curl -s "http://localhost:8080/navigate?tab_id=ABC123&url=https://vercel.com"
# 4. Get accessibility snapshot (token-optimized for LLMs)
curl -s "http://localhost:8080/snapshot?tab_id=ABC123&filter=interactive"
# β [{"ref":"e0","role":"link","name":"VercelLogotype"},
# {"ref":"e1","role":"button","name":"Ask AI"}, ...]
All endpoints return JSON. Optional auth via KURI_SECRET env var.
| Path | Description |
|---|---|
GET /health |
Server status, tab count, version |
GET /tabs |
List all registered tabs |
GET /discover |
Auto-discover Chrome tabs via CDP |
GET /browdie |
π° (easter egg) |
| Path | Params | Description |
|---|---|---|
GET /navigate |
tab_id, url |
Navigate tab to URL |
GET /tab/new |
url |
Create a new tab |
GET /window/new |
url |
Create a new window/tab target |
GET /snapshot |
tab_id, filter, format |
A11y tree snapshot with @eN refs |
GET /text |
tab_id |
Extract page text |
GET /screenshot |
tab_id, format, quality |
Capture screenshot (base64) |
GET /action |
tab_id, ref, kind |
Click/type/scroll by ref |
GET /evaluate |
tab_id, expression |
Execute JavaScript |
GET /close |
tab_id |
Close tab + cleanup |
| Path | Description |
|---|---|
GET /markdown |
Convert page to Markdown |
GET /links |
Extract all links |
GET /dom/query |
CSS selector query |
GET /dom/html |
Get element HTML |
GET /pdf |
Print page to PDF |
| Path | Description |
|---|---|
GET /har/start?tab_id= |
Start recording network traffic |
GET /har/stop?tab_id= |
Stop + return HAR 1.2 JSON |
GET /har/status?tab_id= |
Recording state + entry count |
GET /har/replay?tab_id=&filter=api&format=all |
API map with curl/fetch/python code snippets |
| Path | Description |
|---|---|
GET /back |
Browser back |
GET /forward |
Browser forward |
GET /reload |
Reload page |
GET /cookies |
Get cookies |
GET /cookies/delete |
Delete cookies |
GET /cookies/clear |
Clear all cookies |
GET /storage/local |
Get localStorage |
GET /storage/session |
Get sessionStorage |
GET /storage/local/clear |
Clear localStorage |
GET /storage/session/clear |
Clear sessionStorage |
GET /session/save |
Save browser session |
GET /session/load |
Restore browser session |
GET /session/list |
List saved browser sessions |
GET /auth/profile/save |
Save cookies + storage as a named auth profile |
GET /auth/profile/load |
Restore a named auth profile into a tab |
GET /auth/profile/list |
List saved auth profiles |
GET /auth/profile/delete |
Delete a saved auth profile |
GET /debug/enable |
Enable in-page debug HUD and optional freeze mode |
GET /debug/disable |
Disable in-page debug HUD |
GET /headers |
Set custom request headers |
GET /perf/lcp |
Capture Largest Contentful Paint timing, optionally after navigation |
On macOS, auth profile secrets are stored in the user Keychain. On other platforms, Kuri falls back to .kuri/auth-profiles/.
url and expression query params are percent-decoded by the server, so encoded values like https%3A%2F%2Fexample.com are accepted.
| Path | Description |
|---|---|
GET /diff/snapshot |
Delta diff between snapshots |
GET /emulate |
Device emulation |
GET /geolocation |
Set geolocation |
POST /upload |
File upload |
GET /script/inject |
Inject JavaScript |
GET /intercept/start |
Start request interception |
GET /intercept/stop |
Stop interception |
GET /screenshot/annotated |
Screenshot with element annotations |
GET /screenshot/diff |
Visual diff between screenshots |
GET /screencast/start |
Start screencast |
GET /screencast/stop |
Stop screencast |
GET /video/start |
Start video recording |
GET /video/stop |
Stop video recording |
GET /console |
Get console messages |
GET /stop |
Stop page loading |
GET /get |
Direct HTTP fetch (server-side) |
GET /scrollintoview |
Scroll a referenced element into view |
GET /drag |
Drag from one ref to another |
GET /keyboard/type |
Type text with key events |
GET /keyboard/inserttext |
Insert text directly |
GET /keydown |
Dispatch a keydown event |
GET /keyup |
Dispatch a keyup event |
GET /wait |
Wait for ready state or element conditions |
GET /tab/close |
Close a tab |
GET /highlight |
Highlight an element by ref or selector |
GET /errors |
Get page/runtime errors |
GET /set/offline |
Toggle offline network emulation |
GET /set/media |
Set emulated media features |
GET /set/credentials |
Set HTTP basic auth credentials |
GET /find |
Find text matches in the current page |
GET /trace/start |
Start Chrome tracing |
GET /trace/stop |
Stop tracing and return trace data |
GET /profiler/start |
Start JS profiler |
GET /profiler/stop |
Stop JS profiler |
GET /inspect |
Inspect an element or page state |
GET /set/viewport |
Set viewport size |
GET /set/useragent |
Override user agent |
GET /dom/attributes |
Get element attributes |
GET /frames |
List frame tree |
GET /network |
Inspect network state/requests |
Kuri applies anti-detection patches automatically on startup β no manual config needed.
Page.addScriptToEvaluateOnNewDocument β stealth patches run before any page JS--disable-blink-features=AutomationControlled)Navigate auto-detects blocks and returns structured fallback:
curl -s "http://localhost:8080/navigate?tab_id=ABC&url=https://protected-site.com"
# If blocked:
# {"blocked":true,"blocker":"akamai","ref_code":"0.7d...",
# "fallback":{"suggestions":["Open URL directly in browser","Use KURI_PROXY"]}}
# If ok: normal CDP response
Detects: Akamai, Cloudflare, PerimeterX, DataDome, generic captcha.
KURI_PROXY=socks5://user:pass@residential-proxy:1080 ./zig-out/bin/kuri
KURI_PROXY=http://proxy:8080 ./zig-out/bin/kuri
| Site | Protection | Result |
|---|---|---|
| Singapore Airlines | Akamai WAF | β Bypassed (was blocked before v0.4) |
| Shopee SG | Custom anti-fraud | β Page loads, redirects to login |
| Google Flights | None | β Full interaction |
| Booking.com | PerimeterX | β οΈ Needs proxy |
Standalone HTTP fetcher β no Chrome, no Playwright, no npm. Ships as a ~2 MB binary with built-in QuickJS for JS execution.
zig build fetch # build + run
# Default: convert to Markdown
kuri-fetch https://example.com
# Extract links
kuri-fetch -d links https://news.ycombinator.com
# Structured JSON output
kuri-fetch --json https://example.com
# Execute inline scripts via QuickJS
kuri-fetch --js https://example.com
# Write to file, quiet mode
kuri-fetch -o page.md -q https://example.com
# Pipe-friendly: content β stdout, status β stderr
kuri-fetch -d text https://example.com | wc -w
markdown, html, links, text, json--js executes inline <script> tagsdocument.querySelector, getElementById, window.location, document.title, console.log, setTimeout (SSR-style)NO_COLOR, TERM=dumb, --no-color, TTY detection-o / --output with byte count + timing summary--user-agent flag-q suppresses stderr statusInteractive terminal browser β browse the web from your terminal. No Chrome needed.
zig build browse # build + run
kuri-browse https://example.com
π° kuri-browse β terminal browser
β loading https://example.com
# Example Domain
This domain is for use in documentation examples...
Learn more [1]
βββββ Links βββββ
[1] https://iana.org/domains/example
β 528 bytes, 1 links (133ms)
[nav] https://example.com> 1 β type 1 to follow the link
| Command | Action |
|---|---|
<number> |
Follow link [N] |
<url> |
Navigate (if contains .) |
:go <url> |
Navigate to URL |
:back, :b |
Go back in history |
:forward, :f |
Go forward |
:reload, :r |
Re-fetch current page |
:links, :l |
Show link index |
/<term> |
Search in page (highlights matches) |
:search <t> |
Search in page |
:n, :next |
Re-highlight search |
:history |
Show navigation history |
:help, :h |
Show all commands |
:quit, :q |
Exit |
[N], type the number to follow it/term highlights all matchesjavascript: and mailto: hrefsScriptable CLI for Chrome automation β drives the browser command-by-command from your terminal or shell scripts. Shares session state across invocations via ~/.kuri/session.json.
zig build agent # build kuri-agent
# 1. Find a Chrome tab
kuri-agent tabs
# β ws://127.0.0.1:9222/devtools/page/ABC123 https://example.com
# 2. Attach to it
kuri-agent use ws://127.0.0.1:9222/devtools/page/ABC123
# 3. Navigate + interact
kuri-agent go https://example.com
kuri-agent snap --interactive # β [{"ref":"e0","role":"link","name":"More info"}]
kuri-agent click e0
kuri-agent shot # saves ~/.kuri/screenshots/<ts>.png
| Command | Description |
|---|---|
tabs [--port N] |
List Chrome tabs |
use <ws_url> |
Attach to a tab (saves session) |
status |
Show current session |
go <url> |
Navigate to URL |
snap [--interactive] [--text] [--depth N] |
A11y snapshot, saves @eN refs |
click <ref> |
Click element by ref |
type <ref> <text> |
Type into element |
fill <ref> <text> |
Fill input value |
select <ref> <value> |
Select dropdown option |
eval <js> |
Evaluate JavaScript |
text [selector] |
Get page text |
shot [--out file.png] |
Screenshot |
cookies |
List cookies with security flags |
headers |
Check security response headers |
audit |
Full security audit |
kuri-agent supports browser-native security trajectories β log in once, then run reconnaissance and header/cookie audits without leaving the terminal.
Enumerate β Inspect β after authenticating, dump auth cookies and check security flags:
kuri-agent go https://target.example.com/login
kuri-agent snap --interactive
kuri-agent fill e2 myuser
kuri-agent fill e3 mypassword
kuri-agent click e4 # submit login
kuri-agent cookies
# cookies (3):
# session_id domain=.example.com path=/ [Secure] [HttpOnly] [SameSite=Strict]
# csrf_token domain=.example.com path=/ [Secure] [!HttpOnly]
# tracking domain=.example.com path=/ [!Secure] [!HttpOnly]
Header audit β check what security headers the target sends:
kuri-agent go https://target.example.com
kuri-agent headers
# β {"url":"https://...","status":200,"headers":{
# "content-security-policy":"default-src 'self'",
# "strict-transport-security":"max-age=31536000",
# "x-frame-options":"(missing)",
# "x-content-type-options":"nosniff", ...}}
Full audit β HTTPS, missing headers, JS-visible cookies in one shot:
kuri-agent audit
# β {"protocol":"https:","url":"https://...","score":6,
# "issues":["MISSING:x-frame-options","COOKIES_EXPOSED_TO_JS:2"],
# "headers":{"content-security-policy":"default-src 'self'", ...}}
Cross-account trajectory β use eval to replay API calls with different tokens:
# After login, grab the auth token from localStorage
kuri-agent eval "localStorage.getItem('token')"
# Probe a resource ID with the current session
kuri-agent eval "fetch('/api/assessments/42').then(r=>r.status)"
# Check for IDOR: does a different user's resource return 200 or 403?
kuri-agent eval "fetch('/api/assessments/99').then(r=>r.status)"
kuri-agent outputs JSON suitable for pipeline integration. Each security command emits a single JSON line β pipe through jq for triage:
kuri-agent audit | jq '.issues[]'
kuri-agent cookies | head -20
kuri-agent headers | jq '.headers | to_entries[] | select(.value == "(missing)") | .key'
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HTTP API Layer β
β (std.http.Server, thread-per-connection) β
ββββββββββββββββ¬βββββββββββββββββββ¬βββββββββββββββββββββββββ€
β Browser β Crawler Engine β kuri-fetch / browse β
β Bridge β β (standalone CLIs) β
ββββββββββββββββΌβββββββββββββββββββΌβββββββββββββββββββββββββ€
β CDP Client β URL Validator β std.http.Client β
β Tab Registry β HTMLβMarkdown β QuickJS JS Engine β
β A11y Snapshotβ Link Extractor β DOM Stubs (Layer 3) β
β Ref Cache β Text Extractor β SSRF Validator β
β HAR Recorder β β Colored Renderer β
β Stealth JS β β History + REPL β
ββββββββββββββββ΄βββββββββββββββββββ΄βββββββββββββββββββββββββ€
β Chrome Lifecycle Manager β
β (launch, health-check, auto-restart, port detection) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
deinit() callGeneralPurposeAllocator in debug mode catches every leakLauncher β Bridge β CdpClients β HarRecorders β Snapshots β Tabserrdefer guards β partial failures roll back cleanly| Mode | Behavior |
|---|---|
Managed (no CDP_URL) |
Launches Chrome headless, finds free CDP port, supervises, auto-restarts on crash (max 3 retries), kills on shutdown |
External (CDP_URL set) |
Connects to existing Chrome, health-checks via /json/version, does NOT kill on shutdown |
kuri/
βββ build.zig # Build system (Zig 0.15.2)
βββ build.zig.zon # Package manifest + QuickJS dep
βββ src/
β βββ main.zig # CDP server entry point
β βββ fetch_main.zig # kuri-fetch CLI entry point
β βββ browse_main.zig # kuri-browse CLI entry point
β βββ js_engine.zig # QuickJS wrapper + DOM stubs
β βββ bench.zig # Benchmark harness
β βββ chrome/
β β βββ launcher.zig # Chrome lifecycle manager
β βββ server/
β β βββ router.zig # HTTP route dispatch (40+ endpoints)
β β βββ middleware.zig # Auth (constant-time comparison)
β β βββ response.zig # JSON response helpers
β βββ bridge/
β β βββ bridge.zig # Central state (tabs, CDP, HAR, snapshots)
β β βββ config.zig # Env var configuration
β βββ cdp/
β β βββ client.zig # CDP WebSocket client
β β βββ websocket.zig # WebSocket frame codec
β β βββ protocol.zig # CDP method constants
β β βββ actions.zig # High-level CDP actions
β β βββ stealth.zig # Bot detection bypass
β β βββ har.zig # HAR 1.2 recorder
β βββ snapshot/
β β βββ a11y.zig # A11y tree with interactive filter
β β βββ diff.zig # Snapshot delta diffing
β β βββ ref_cache.zig # @eN ref β node ID cache
β βββ crawler/
β β βββ validator.zig # SSRF defense, URL validation
β β βββ markdown.zig # HTML β Markdown (SIMD tag counting)
β β βββ fetcher.zig # Page fetching
β β βββ extractor.zig # Readability extraction
β β βββ pipeline.zig # Parallel crawl pipeline
β βββ storage/
β β βββ local.zig # Local file writer
β β βββ r2.zig # R2/S3 uploader
β βββ util/
β β βββ json.zig # JSON helpers
β βββ test/
β βββ harness.zig # Test HTTP client
β βββ integration.zig # Integration tests
β βββ merjs_e2e.zig # E2E tests
βββ js/
βββ stealth.js # Bot detection bypass
βββ readability.js # Content extraction
| Env Var | Default | Description |
|---|---|---|
HOST |
127.0.0.1 |
Server bind address |
PORT |
8080 |
Server port |
CDP_URL |
(none) | Connect to existing Chrome (ws://... or http://127.0.0.1:9222) |
KURI_SECRET |
(none) | Auth secret for API requests |
STATE_DIR |
.kuri |
Session state directory |
REQUEST_TIMEOUT_MS |
30000 |
HTTP request timeout |
NAVIGATE_TIMEOUT_MS |
30000 |
Navigation timeout |
STALE_TAB_INTERVAL_S |
30 |
Stale tab cleanup interval |
NO_COLOR |
(none) | Disable colored CLI output |
For a 50-page monitoring task (from Pinchtab benchmarks):
| Method | Tokens | Cost ($) | Best For |
|---|---|---|---|
/text |
~40,000 | $0.20 | Read-heavy (13Γ cheaper than screenshots) |
/snapshot?filter=interactive |
~180,000 | $0.90 | Element interaction |
/snapshot (full) |
~525,000 | $2.63 | Full page understanding |
/screenshot |
~100,000 | $1.00 | Visual verification |
Open an issue before submitting a large PR so we can align on the approach.
git clone https://github.com/justrach/kuri.git
cd kuri
zig build test # 230+ tests must pass
zig build test-fetch # kuri-fetch tests (66 tests)
zig build test-browse # kuri-browse tests
See CONTRIBUTORS.md for guidelines.
| Project | What we borrowed |
|---|---|
| agent-browser | @eN ref system, snapshot diffing, HAR recording patterns |
| Pinchtab | Browser control architecture for AI agents |
| Pathik | High-performance crawling patterns |
| QuickJS-ng via mitchellh/zig-quickjs-ng | JS engine for kuri-fetch |
| Lightpanda | Zig-native headless browser pioneer, CDP compatibility patterns |
| Zig 0.15.2 | The whole stack |
Apache-2.0