kuri
Kuri 🌰
curl -fsSL https://raw.githubusercontent.com/justrach/kuri/main/install.sh | sh
macOS arm64/x86_64 and Linux x86_64/arm64. Single binary, no runtime deps.
Direct downloads: macOS arm64 · macOS x86_64 · Linux x86_64 · Linux arm64
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 · iOS + Android device control
Quick Start · Benchmarks · kuri-agent · Security Testing · API · Skills · Changelog
Why teams switch to Kuri: current Apple Silicon
ReleaseFastbuilds stay sub-2 MB per binary, and a fresh Google Flights rerun on 2026-04-23 measured 3,392 tokens for a fullkuri-agentloop (go→snap→click→snap→eval). Cross-tool deltas should be rerun in the same environment before quoting a percentage.
Why Kuri Wins for Agents
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.
- 135 HTTP endpoints — full parity with agent-browser and browser-use, from React inspection to Core Web Vitals.
- 7-12% fewer tokens than agent-browser on real pages thanks to
@eNref format and zero-prefix rendering. - 44x lighter observations with
/page/state(48 tokens) vs full snapshot (2,124 tokens) for the same Google Flights page. - Batch execution —
POST /batchsends N commands in one HTTP call, eliminating N-1 round-trips and N-1 LLM turns. - React-compatible — trusted CDP mouse events and per-character key events fire React 18/19
onClickandonChange.
Snapshot tokens: Google Flights SIN → TPE
Fresh rerun on 2026-05-24 in this workspace, measured with wc -c and chars/4 approximation.
| Tool / Mode | Chars | ~Tokens | Note |
|---|---|---|---|
kuri snap (full) |
8,499 | 2,124 | All nodes + interactive refs |
kuri snap (interactive only) |
~3,000 | ~750 | Best for agent loops |
kuri /page/state |
190 | 48 | Lightweight observation (url, title, scroll%, counts) |
| agent-browser snap (estimated) | ~9,183 | ~2,295 | [ref=e0] format overhead |
Token efficiency: kuri vs agent-browser
| Page | kuri tokens | agent-browser tokens | Savings |
|---|---|---|---|
| example.com | 40 | 35 | -13% (trivial page, agent-browser skips root) |
| Hacker News | 386 | ~440 | 12% fewer |
| Google Flights SIN→TPE | 2,124 | ~2,295 | 7% fewer |
The savings come from kuri's compact format:
@e0refs (3 chars) vs[ref=e0](9 chars)- No
-prefix per line (saves 2 chars × line count) - Same indentation, same node filtering
Full workflow cost: go → snap → click → snap → eval
| Tool | Tokens per cycle |
|---|---|
| kuri-agent | ~3,400 |
With /page/state instead of second snap |
~1,700 |
With POST /batch (all in one call) |
~1,700 (same tokens, 1 HTTP call instead of 5) |
Binary size and memory
Measured on Apple M4 Pro, macOS 26.4.1. Current binaries were built with -Doptimize=ReleaseFast.
| Binary | Current size |
|---|---|
kuri |
1,093,840 B (1.04 MiB) |
kuri-agent |
629,904 B (615 KiB) |
kuri-browse |
1,089,120 B (1.04 MiB) |
kuri-fetch |
2,063,488 B (1.97 MiB) |
RSS stayed flat across the Zig 0.16 migration
Measured against the current v0.4.3 ReleaseFast build with /usr/bin/time -l.
| Command | v0.4.3 mean max RSS |
|---|---|
kuri-fetch --version |
~2.45 MiB |
kuri-browse --version |
~2.45 MiB |
kuri-fetch --quiet --dump markdown http://example.com/ |
~9.17 MiB |
The Problem
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)
📦 Installation
One-line install (macOS / Linux)
curl -fsSL https://raw.githubusercontent.com/justrach/kuri/release-channel/stable/install.sh | sh
Detects your platform, downloads the right binary, installs to ~/.local/bin.
Downloads come from Kuri's self-managed release-channel branch. macOS binaries are locally signed with a Developer ID certificate. GitHub Release assets mirror these same tarballs.
bun / npm
bun install -g kuri-agent
# or: npm install -g kuri-agent
Downloads the correct native binary for your platform at install time.
Release channel
Kuri's stable binaries live on the release-channel branch and are served directly from GitHub raw URLs.
- Stable installer:
https://raw.githubusercontent.com/justrach/kuri/release-channel/stable/install.sh - Stable manifest:
https://raw.githubusercontent.com/justrach/kuri/release-channel/stable/latest.json - Branch view:
https://github.com/justrach/kuri/tree/release-channel/stable - Direct download pattern:
https://raw.githubusercontent.com/justrach/kuri/release-channel/stable/<version>/kuri-<version>-<target>.tar.gz
Manual
Download the tarball for your platform from the stable release manifest or from the GitHub Releases page and unpack it to your $PATH.
Stable install URL:
curl -fsSL https://raw.githubusercontent.com/justrach/kuri/release-channel/stable/install.sh | sh
The manifest includes exact asset URLs plus SHA-256 checksums for aarch64-linux, x86_64-linux, aarch64-macos, and x86_64-macos.
Platform support
| Platform | Status |
|---|---|
macOS (aarch64, x86_64) |
Prebuilt binaries, signed + notarized |
Linux (aarch64, x86_64) |
Prebuilt binaries |
Windows (x86_64) |
Experimental — cross-compile only. zig build -Dtarget=x86_64-windows-gnu is CI-verified, but Chrome automation, daemonization, signal-based shutdown, HAR recording, and the file-backed auth store are all stubbed with error.UnsupportedOnWindows at runtime. Use WSL2 if you need the real feature set. Tracked at #153. |
Kuri leans on POSIX primitives (fork, clock_gettime, raw sockets) in several spots, so a full native Windows port is real work. The compile-level baseline above lets the --version/--help paths and pure in-memory operations run; the gnarly bits (Chrome, sockets, daemonize) need real Win32 implementations before they come off the stub list. If you want to take any of those on, +1 #153 or open a PR.
Build from source
Requires Zig ≥ 0.16.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
⚡ Quick Start
Requirements: Zig ≥ 0.16.0 · Chrome/Chromium (for CDP mode)
git clone https://github.com/justrach/kuri.git
cd kuri
zig build # build everything
zig build test # run 252+ 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
# Experimental standalone browser runtime — separate build, not production
(cd kuri-browser && zig build run -- render https://example.com)
(cd kuri-browser && zig build run -- bench --offline)
First run, shortest path
# 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
Session-first agent loop
For agent-style HTTP usage, prefer a session header plus /tab/new, /page/info, and /snapshot instead of repeating tab_id on every call.
SESSION=hn-demo
BASE=http://127.0.0.1:8080
curl -s -H "X-Kuri-Session: $SESSION" \
"$BASE/tab/new?url=https%3A%2F%2Fnews.ycombinator.com"
curl -s -H "X-Kuri-Session: $SESSION" "$BASE/page/info"
SNAP=$(curl -s -H "X-Kuri-Session: $SESSION" "$BASE/snapshot?filter=interactive&format=compact")
MORE_REF=$(printf '%s' "$SNAP" | python3 -c 'import re,sys; print(re.search(r"\"More\" @(e\\d+)", sys.stdin.read()).group(1))')
curl -s -H "X-Kuri-Session: $SESSION" "$BASE/action?action=click&ref=$MORE_REF"
curl -s -H "X-Kuri-Session: $SESSION" "$BASE/page/info"
There is also a thin experimental wrapper at tools/kuri_harness.py if you want Python helpers on top of the same HTTP surface.
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"}, ...]
🌐 HTTP API
All endpoints return JSON. Optional auth via KURI_SECRET env var. 135 endpoints — full parity with agent-browser and browser-use.
Core
| Path | Description |
|---|---|
GET /health |
Server status, tab count, version |
GET /tabs |
List all registered tabs |
GET /discover |
Auto-discover Chrome tabs via CDP |
GET /tab/current |
Get or set the current tab for an X-Kuri-Session |
GET /page/info |
Live URL/title/ready-state/viewport/scroll for the active tab |
GET /page/state |
Compact page observation: url, title, scroll%, viewport, counts of forms/links/images/inputs |
POST /batch |
Execute multiple commands in one HTTP call — returns array of results |
GET /browdie |
🌰 (easter egg) |
Browser Control
| Path | Params | Description |
|---|---|---|
GET /navigate |
tab_id, url |
Navigate tab to URL |
GET /tab/new |
url, activate, wait |
Create a new tab and optionally hydrate/set current tab |
GET /tab/close |
tab_id |
Close a tab |
GET /window/new |
url, activate, wait |
Create a new window/tab target |
GET /snapshot |
tab_id, filter, format |
A11y tree snapshot with eN refs. Use filter=interactive&format=compact for low-token agent loops. |
GET /text |
tab_id |
Extract page text |
GET /screenshot |
tab_id, format, quality |
Capture screenshot (base64) |
GET /screenshot/annotated |
tab_id |
Screenshot with numbered element labels |
GET /screenshot/diff |
tab_id, baseline |
Visual diff between current and baseline screenshots |
GET /action |
tab_id, ref, action, value |
Click/type/fill/select/scroll/hover/dblclick/check/uncheck/blur by ref |
GET /evaluate |
tab_id, expression |
Execute JavaScript |
GET /evalhandle |
tab_id, expression |
Execute JS, return objectId handle (not value) |
GET /close |
tab_id |
Close tab + cleanup |
GET /bringtofront |
tab_id |
Bring tab to front |
Actions
| Path | Params | Description |
|---|---|---|
GET /clear |
ref |
Clear input field value |
GET /selectall |
ref |
Select all text in input/contenteditable |
GET /setvalue |
ref, value |
Set input value directly (bypasses key events) |
GET /dispatch |
ref, type |
Dispatch custom DOM event on element |
GET /boundingbox |
ref |
Get element bounding rect (x, y, width, height, centerX, centerY) |
GET /getattribute |
ref, name |
Get element attribute by name |
GET /inputvalue |
ref |
Get current input element value |
GET /element/state |
ref, check |
Quick boolean: exists, visible, enabled, checked |
GET /find-element |
text/role/label/placeholder/testid |
Semantic locator — find element without snap |
GET /highlight |
ref or selector |
Highlight element with overlay |
Mouse & Touch
| Path | Params | Description |
|---|---|---|
GET /mouse/move |
x, y |
Move mouse to coordinates |
GET /mouse/down |
x, y, button |
Mouse button down |
GET /mouse/up |
x, y, button |
Mouse button up |
GET /mouse/wheel |
x, y, deltaX, deltaY |
Mouse wheel scroll |
GET /tap |
x, y |
Touch tap (touchStart + touchEnd) |
GET /swipe |
startX, startY, endX, endY |
Touch swipe gesture |
GET /drag |
src_ref, tgt_ref |
Drag element to target |
Keyboard
| Path | Params | Description |
|---|---|---|
GET /keyboard/type |
tab_id, text |
Type text via key events |
GET /keyboard/inserttext |
tab_id, text |
Insert text directly |
GET /keydown |
tab_id, key |
Key down event |
GET /keyup |
tab_id, key |
Key up event |
Content Extraction
| 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 /dom/attributes |
Get element attributes |
GET /pdf |
Print page to PDF |
GET /find |
In-page text search |
Waiting
| Path | Params | Description |
|---|---|---|
GET /wait |
selector, text, url, state, visible, timeout |
Wait for selector/text/URL pattern/networkidle/load state |
GET /wait/function |
expression, timeout |
Wait for arbitrary JS expression to be truthy |
GET /wait/download |
timeout |
Wait for file download to complete |
Dialog Handling
| Path | Description |
|---|---|
GET /dialog/auto |
Auto-handle all JS dialogs (accept or dismiss) |
GET /dialog/accept |
Accept current dialog (with optional prompt text) |
GET /dialog/dismiss |
Dismiss current dialog |
Network & HAR
| Path | Description |
|---|---|
GET /har/start |
Start recording network traffic |
GET /har/stop |
Stop + return HAR 1.2 JSON |
GET /har/status |
Recording state + entry count |
GET /har/replay |
API map with curl/fetch/python code snippets |
GET /cookies |
Get cookies |
GET /cookies/set |
Set cookies |
GET /cookies/delete |
Delete cookies |
GET /cookies/clear |
Clear all cookies |
GET /headers |
Set custom request headers |
GET /intercept/start |
Start request interception |
GET /intercept/stop |
Stop request interception |
GET /intercept/requests |
List intercepted requests |
GET /request/detail |
Get response body for a request ID |
GET /response/body |
Fetch URL and return response body |
GET /network |
Network traffic stats |
GET /download |
Trigger file download |
Navigation & State
| Path | Description |
|---|---|
GET /back |
Browser back |
GET /forward |
Browser forward |
GET /reload |
Reload page |
GET /stop |
Stop page loading |
GET /pushstate |
SPA navigation via history.pushState |
GET /storage/local |
Get/set localStorage |
GET /storage/session |
Get/set 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 sessions |
GET /setcontent |
Set page HTML directly (POST) |
Auth Profiles
| Path | Description |
|---|---|
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 /auth/extract |
Extract auth tokens (JWT, cookies, headers) |
GET /set/credentials |
Set HTTP basic auth credentials |
On macOS, auth profile secrets are stored in the user Keychain.
Emulation
| Path | Params | Description |
|---|---|---|
GET /emulate |
device type, screen size | Device emulation |
GET /set/viewport |
width, height |
Set viewport size |
GET /set/useragent |
ua |
Set user agent |
GET /set/media |
media |
Emulate media type |
GET /set/offline |
offline |
Toggle offline mode |
GET /geolocation |
lat, lng |
Override geolocation |
GET /timezone |
timezone |
Override timezone (e.g. America/New_York) |
GET /locale |
locale |
Override locale (e.g. en-US) |
GET /permissions |
name, state |
Grant/deny permissions (geolocation, notifications, clipboard) |
Scripts & Injection
| Path | Description |
|---|---|
GET /script/inject |
Inject JavaScript into page (persists across nav) |
GET /initscript/remove |
Remove a previously injected init script |
GET /addstyle |
Inject CSS stylesheet |
GET /expose |
Expose a named function to page JS context |
React Inspection
| Path | Description |
|---|---|
GET /react/tree |
React component tree via DevTools hook |
GET /react/inspect |
React component props and state |
GET /react/renders |
React render tracking (start/stop) |
GET /react/suspense |
React Suspense boundary status |
Recording & Performance
| Path | Description |
|---|---|
GET /recording/start |
Record user actions (click, input, navigate) |
GET /recording/stop |
Stop recording + return action log |
GET /vitals |
Core Web Vitals (LCP, CLS, FID, TTFB, FCP, domInteractive) |
GET /perf/lcp |
Largest Contentful Paint timing |
GET /trace/start |
Start performance trace |
GET /trace/stop |
Stop trace |
GET /profiler/start |
Start JS profiler |
GET /profiler/stop |
Stop profiler |
Debugging
| Path | Description |
|---|---|
GET /debug/enable |
Enable in-page debug HUD and optional freeze mode |
GET /debug/disable |
Disable in-page debug HUD |
GET /inspect |
Element inspection |
GET /errors |
Collect JS errors |
GET /console |
Read console logs |
GET /frames |
List page frames |
GET /frame |
Switch to iframe context by name or URL |
GET /mainframe |
Switch back to main frame |
GET /diff/snapshot |
Diff accessibility tree snapshots |
GET /diff/url |
Compare two URLs side by side (navigate, snapshot, diff) |
Streaming
| Path | Description |
|---|---|
GET /screencast/start |
Start screen recording |
GET /screencast/stop |
Stop screen recording |
GET /video/start |
Start video capture |
GET /video/stop |
Stop video capture |
GET /ws/start |
WebSocket tunnel start |
GET /ws/stop |
WebSocket tunnel stop |
Agent-friendly loop
The lowest-friction server loop is:
GET /tab/new?url=...GET /page/state(lightweight) orGET /snapshot?filter=interactive&format=compact(full)GET /action?action=click&ref=eN- Repeat — or use
POST /batchfor multi-step operations in one call
url and expression query params are percent-decoded. Send X-Kuri-Session: my-agent to persist tab context server-side.
🧠 Skills
The repo includes a user-extensible skill area:
skills/kuri-skill.mdis the base Kuri HTTP-agent skillskills/custom/is reserved for your own project-specific skillsskills/custom/hackernews-page-2.mdis a concrete example custom skill.claude/skills/kuri-server/SKILL.mdstays in sync for Claude-style repo skills
The base skill now also explains which browser path to use:
kuriHTTP API: production Chrome/CDP automation with sessions, snapshots, actions, HAR, cookies, and screenshotskuri-fetch: standalone no-Chrome fetch/text extractionkuri-browse: interactive terminal browsingkuri-agent: scriptable CLI automation against the Kuri serverkuri-browser/: experimental separate Zig-native browser runtime for parity work
For the experimental browser CLI:
cd kuri-browser
zig build run -- render https://news.ycombinator.com --selector ".titleline a" --dump text
zig build run -- render https://todomvc.com/examples/react/dist/ --js --wait-eval "document.querySelectorAll('.todo-list li').length >= 1"
zig build run -- parity --offline
zig build run -- bench --offline
zig build run -- serve-cdp --port 9333
kuri-browser serve-cdp exposes Chrome-style HTTP discovery plus a minimal WebSocket JSON-RPC router for protocol smoke tests. Runtime eval returns V8-shaped CDP remote objects backed by QuickJS; this does not add a V8 dependency and is not full Playwright/Puppeteer compatibility yet.
Screenshots in kuri-browser currently delegate to the main Kuri/CDP renderer. Start ./zig-out/bin/kuri first, then:
cd kuri-browser
zig build run -- screenshot https://example.com --out example.jpg --compress --kuri-base http://127.0.0.1:8080
--compress captures a PNG baseline and JPEG candidate, writes the smaller file, and reports byte savings. Current local measurement on https://example.com: 20,523 bytes PNG to 18,183 bytes JPEG quality 50, saving 2,340 bytes or 11%.
Advanced
| 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.
What's applied
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
Bot block detection
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.
Proxy support
KURI_PROXY=socks5://user:pass@residential-proxy:1080 ./zig-out/bin/kuri
KURI_PROXY=http://proxy:8080 ./zig-out/bin/kuri
Tested sites
| Site | Protection | Result |
|---|---|---|
| Singapore Airlines | Akamai WAF | ✅ Bypassed (was blocked before v0.4.0) |
| Shopee SG | Custom anti-fraud | ✅ Page loads, redirects to login |
| Google Flights | None | ✅ Full interaction |
| Booking.com | PerimeterX | ⚠️ Needs proxy |
🔧 kuri-fetch
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
Features
- 5 output modes —
markdown,html,links,text,json - QuickJS JS engine —
--jsexecutes inline<script>tags - DOM stubs —
document.querySelector,getElementById,window.location,document.title,console.log,setTimeout(SSR-style) - SSRF defense — blocks private IPs, metadata endpoints, non-HTTP schemes
- Colored output — respects
NO_COLOR,TERM=dumb,--no-color, TTY detection - File output —
-o/--outputwith byte count + timing summary - Custom UA —
--user-agentflag - Quiet mode —
-qsuppresses stderr status
🌐 kuri-browse
Interactive 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
Commands
| 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 |
Features
- Colored markdown rendering — headings, links, code blocks, bold, blockquotes
- Numbered links — every link gets
[N], type the number to follow it - Navigation history — back/forward like a real browser
- In-page search —
/termhighlights all matches - Relative URL resolution — follows links naturally across pages
- Smart filtering — skips
javascript:andmailto:hrefs
🤖 kuri-agent
Scriptable 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
Commands
| Command | Description |
|---|---|
tabs [--port N] |
List Chrome tabs |
use <ws_url> |
Attach to a tab (saves session) |
open [url] [--port N] |
Open a new tab (optionally navigating to url) |
status |
Show current session |
go <url> |
Navigate to URL |
snap [--interactive] [--json] [--text] [--depth N] |
A11y snapshot, saves eN refs |
click <ref> |
Click element by ref (CDP mouse events, React-compatible) |
type <ref> <text> |
Type into element (per-character key events, React-compatible) |
fill <ref> <text> |
Fill input value |
select <ref> <value> |
Select dropdown option |
hover <ref> |
Hover over element |
focus <ref> |
Focus element |
scroll |
Scroll the page |
viewport [width height] |
Get or set viewport dimensions |
eval <js> |
Evaluate JavaScript |
text [selector] |
Get page text |
shot [--out file.png] |
Screenshot |
back |
Navigate back |
forward |
Navigate forward |
reload |
Reload current page |
cookies |
List cookies with security flags |
headers |
Check security response headers |
audit |
Full security audit |
storage [local|session|all] |
Dump localStorage / sessionStorage |
jwt |
Extract and decode JWTs from cookies and storage |
fetch <method> <url> [--data <json>] |
Authenticated fetch using page cookies |
probe <url-template> <start> <end> |
IDOR probe: iterate numeric IDs in URL |
grab <ref> |
Click ref, intercept window.open, follow redirect in-tab |
wait-for-tab [--port N] |
Poll for a new tab, auto-switch session |
stealth |
Apply anti-detection patches |
set-header <name> <value> |
Add a custom header to all requests |
show-headers |
Show stored extra headers |
clear-headers |
Remove all extra headers |
📱 kuri-mobile (iOS + Android)
Native Zig CLI for driving iOS Simulators, real iPhones (listing + launch/terminate), and Android devices/emulators — inspired by mobile-device-mcp, reimplemented in Zig with no Bun/Node/Gradle/Xcode in the build path.
cd kuri-mobile && zig build && cp zig-out/bin/kuri-mobile ../zig-out/bin/
# The main `kuri` binary forwards android/ios subcommands to kuri-mobile:
kuri ios list-devices # sims + real devices (usbmuxd, native)
kuri ios openurl https://example.com # navigate Safari
kuri ios screenshot out.png # auto-picks booted sim
kuri ios launch com.apple.Preferences
kuri android list-devices # native Zig adb wire-protocol client
kuri android tap 540 1200
kuri android swipe 100 1500 100 500
kuri android screenshot phone.png
kuri android uitree # flat element list via uiautomator dump
What's native Zig: adb host protocol (libc sockets, 4-hex framing over host:transport:/shell:/exec:), Android XML UI tree parser, usbmuxd ListDevices plist client.
What shells out: xcrun simctl (iOS Simulator), xcrun devicectl (iOS real-device launch/terminate).
Driverless by design: no on-device app is installed, so run_code sandboxes and XCUITest-backed tap/uitree on real iOS devices are intentionally not available. See kuri-mobile/README.md for the full parity matrix vs upstream.
🔒 Security Testing
kuri-agent supports browser-native security trajectories — log in once, then run reconnaissance and header/cookie audits without leaving the terminal.
Trajectories
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)"
Trajectory Report Format
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'
🏗 Architecture
┌──────────────────────────────────────────────────────────┐
│ 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) │
└──────────────────────────────────────────────────────────┘
Memory Model
- Arena-per-request — all per-request memory freed in one
deinit()call - No GC —
GeneralPurposeAllocatorin debug mode catches every leak - Proper cleanup chains —
Launcher → Bridge → CdpClients → HarRecorders → Snapshots → Tabs errdeferguards — partial failures roll back cleanly
Chrome Lifecycle
| 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 |
📁 Structure
kuri/
├── build.zig # Build system (Zig 0.16.0)
├── 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
├── kuri-browser/ # Native Zig rendering experiments
└── kuri-mobile/ # iOS + Android device control (Zig-native adb + usbmuxd)
├── src/
│ ├── common/ # io helpers, unified UI tree parser
│ ├── android/ # adb wire protocol client, driver, CLI
│ └── ios/ # simctl, usbmuxd, devicectl, CLI
└── README.md # Full parity matrix vs mobile-device-mcp
⚙️ Configuration
| 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 |
💰 Token Cost
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&format=compact |
~40,000 | $0.20 | Low-token element interaction |
/snapshot (full) |
~525,000 | $2.63 | Full page understanding |
/screenshot |
~100,000 | $1.00 | Visual verification |
🤝 Contributing
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 # 252+ tests must pass
zig build test-fetch # kuri-fetch tests (69 tests)
zig build test-browse # kuri-browse tests (22 tests)
See CONTRIBUTORS.md for guidelines.
Credits
| 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.16.0 | The whole stack |
License
Apache-2.0
