justrach/kuri: Browser automation and web crawling for AI agents. Zig-native, token-efficient CDP snapshots, HAR recording, and a standalone fetcher. · GitHub

💥 Explore this insightful post from Hacker News 📖

📂 **Category**:

📌 **What You’ll Learn**:

Kuri

Release
License
Zig
node_modules
status

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.

  • The product story is not “most commands.” It is “useful state from real pages at the lowest model cost.”
  • A tiny output only counts if the page actually rendered. Empty-shell output is a failure mode, not a win.
  • The best proof is same-page, same-session, same-tokenizer comparisons.

Snapshot tokens: Google Flights SIN → TPE

Same 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

Full workflow cost: 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 (🔥) 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)

One-line install (macOS / Linux)

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

Browse vercel.com in 4 commands

# 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

HAR Recording & API Replay

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


🛡️ Stealth & Bot Evasion

Kuri applies anti-detection patches automatically on startup — no manual config needed.

  • Page.addScriptToEvaluateOnNewDocument — stealth patches run before any page JS
  • navigator.webdriver = false — hides automation flag at Chromium level (--disable-blink-features=AutomationControlled)
  • WebGL/Canvas/AudioContext spoofing — defeats fingerprint-based detection
  • UA rotation — 5 realistic Chrome/Safari/Firefox user agents
  • chrome.csi/chrome.loadTimes — stubs for Akamai-specific checks

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
  • 5 output modesmarkdown, html, links, text, json
  • QuickJS JS engine--js executes inline