curl -fsSL https://raw.githubusercontent.com/justrach/kuri/main/install.sh | shmacOS 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.
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.
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 |
| 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
| 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) |
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) |
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 |
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/release-channel/stable/install.sh | shDetects 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 install -g kuri-agent
# or: npm install -g kuri-agentDownloads the correct native binary for your platform at install time.
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
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 | shThe manifest includes exact asset URLs plus SHA-256 checksums for aarch64-linux, x86_64-linux, aarch64-macos, and x86_64-macos.
| 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.
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-browseRequirements: 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)# 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/tabsFor 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# 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. 135 endpoints β full parity with agent-browser and browser-use.
| 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) |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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) |
| 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.
| 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) |
| 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 |
| 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 |
| 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 |
| 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) |
| 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 |
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.
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 9333kuri-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%.
| 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- 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 responseDetects: 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.0) |
| Shopee SG | Custom anti-fraud | β Page loads, redirects to login |
| Google Flights | None | β Full interaction |
| Booking.com | PerimeterX |
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 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
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
| 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 |
- 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
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| 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 |
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 dumpWhat'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.
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) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- 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
| 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.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
| 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&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 |
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.
| 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 |
Apache-2.0
