A fast CLI for browser automation with built-in stealth. Wraps Playwright behind a persistent daemon on a Unix socket — first call cold-starts in ~3s, subsequent calls run in under 30ms (warm daemon).
Built for AI agents doing QA and web scraping, but works just as well by hand.
Why Browse? — what makes it different for AI agent builders, DevOps, QA, security, accessibility, and more.
# Install
brew install forjd/tap/browse
bun x patchright install chrome
# Navigate and screenshot in 2 commands
browse goto https://example.com
browse screenshot
# Or automate with refs
browse snapshot # see @e1, @e2, @e3...
browse fill @e1 "hello@example.com"
browse click @e2- Install
- GitHub Actions
- Docker
- System Requirements
- Core Concepts
- AI Agent Integration
- Common Tasks
- Advanced
- Plugins
- Configuration
- Architecture & Stealth
- Performance
- Commands Reference
- Troubleshooting
- License
brew install forjd/tap/browse
bun x patchright install chromeRequires Bun >= 1.0.
# Via script
curl -fsSL https://raw.githubusercontent.com/forjd/browse/main/install.sh | bash
# Or manually
git clone https://github.com/forjd/browse.git
cd browse
./setup.shThis compiles a self-contained binary to dist/browse and symlinks it to ~/.local/bin/browse.
If you use Claude Code, install the skill:
bunx skills add forjd/browseUse the official composite action to run browse in CI with Bun dependencies and Patchright browser binaries cached between runs:
jobs:
qa:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: forjd/browse/.github/actions/browse@main
with:
command: healthcheck --reporter junit --out results.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: browse-results
path: results.xmlInputs:
| Input | Default | Description |
|---|---|---|
command |
healthcheck |
Browse command to run |
config |
browse.config.json |
Path to the config file in your repository |
bun-version |
1.3.11 |
Bun version installed by the action |
browser |
chrome |
Patchright browser channel installed by the action |
The action executes browse from the action checkout, but runs it against your repository workspace so relative config paths, flows, and output files still behave normally.
Linux and macOS runners are supported today. Windows support is tracked separately in the platform roadmap.
The repository Dockerfile builds a runtime image with browse, Bun-built dependencies, and the Playwright system packages already installed.
docker build -t browse .
# Run against the current project directory
docker run --rm -it -v "$PWD":/work -w /work browse healthcheck
# Or use it as a disposable CLI for ad-hoc browser automation
docker run --rm -it -v "$PWD":/work -w /work browse goto https://example.comThe image uses a multi-stage build, copies only the files needed to compile the binary, and ships with a .dockerignore so local docs, tests, and Git metadata do not bloat the build context.
| Platform | Version | Notes |
|---|---|---|
| macOS | 12+ (Monterey) | Apple Silicon or Intel |
| Linux | glibc 2.31+ | Ubuntu 20.04+, Debian 11+ |
| Windows | Not supported | Use WSL2 |
Resources:
- Disk: ~500MB for Chromium
- RAM: ~150MB for daemon, ~300MB per page
- Socket: Unix domain socket at
/tmp/browse-daemon.sock
Refs (@e1, @e2, ...) are how you target elements. Run browse snapshot to assign them, then use them with click, fill, and select. Refs go stale after navigation — just snapshot again.
browse snapshot # assigns @e1, @e2, @e3, ...
browse fill @e3 "search term"
browse click @e4
browse snapshot # re-assign after the page changesSample output:
[page] "Example Domain"
@e1 [link] "Learn more"
@e2 [button] "Submit"
@e3 [textbox] "Email address"
browse goto https://example.com # navigate — daemon starts automatically
browse goto https://example.com --preset mobile # mobile viewport
browse url # print the current page URL
browse back # navigate back in history
browse forward # navigate forward in history
browse reload # reload current page
browse reload --hard # reload bypassing cachebrowse click @e1 # click an element by ref
browse hover @e3 # hover over an element by ref
browse press Tab # send keyboard key press
browse press Shift+Tab # key combination
browse press Escape # close modals/popovers
browse fill @e2 "hello" # type into an input
browse upload @e5 /path/to/file.pdf # set file on a file input
browse scroll down # scroll down one viewport height
browse scroll @e3 # scroll element into view
browse scroll 0 500 # scroll to absolute x,y coordinatesUseful for SPAs where client-side navigation doesn't trigger full page loads:
browse wait url /dashboard # wait until URL contains string
browse wait text "Welcome" # wait until text appears on page
browse wait visible .dashboard # wait until element is visible
browse wait hidden .spinner # wait until element disappears
browse wait network-idle # wait until no pending requests
browse wait 2000 # simple delay (last resort)All wait subcommands respect --timeout and error if the condition isn't met in time.
Browse is designed as a browser backend for AI agents. The CLI interface, JSON responses, persistent daemon, and built-in stealth make it a drop-in browser layer for agent frameworks like OpenClaw, Claude Code, and custom pipelines.
Why agents prefer Browse over Playwright/Selenium directly:
| Feature | Browse | Raw Playwright |
|---|---|---|
| Startup time | ~3s cold, <30ms warm | ~3s every call |
| CLI interface | Yes — easy to shell out | No — requires Node.js wrapper |
| Stealth | Built-in, passes bot detection | Requires patches/plugins |
| JSON output | Native --json flag |
Manual serialization |
| Session management | Named sessions via CLI | Code-only |
| Resource usage | Shared daemon | New process per call |
Agents get sub-30ms command latency, named sessions for parallel work, and headless Chrome that passes bot detection — no browser config needed.
browse screenshot [path] # full-page (auto-names if no path given)
browse screenshot --viewport # viewport only
browse screenshot --diff baseline.png # compare against baseline
browse screenshot --diff baseline.png --threshold 5 # custom sensitivity
browse screenshots list # list saved screenshots
browse report --out report.html # generate HTML report from screenshotsThe --diff flag compares the new screenshot against a baseline image and produces a similarity score, diff pixel count, and a visual diff image highlighting changed regions in red.
browse a11y # full page audit, human-readable output
browse a11y --standard wcag2aa # WCAG 2.0 AA rules only
browse a11y --standard wcag21aa # WCAG 2.1 AA rules
browse a11y @e5 # audit a specific element by ref
browse a11y --json # machine-readable output for CI
browse a11y --include ".main" # scope to CSS selector
browse a11y --exclude ".ads" # exclude regionsOutput lists violations grouped by severity (critical, serious, moderate, minor) with the failing rule, affected elements, and a link to the fix guidance. Powered by axe-core.
browse perf # Core Web Vitals + timing metrics
browse perf --json # machine-readable output
browse perf --budget lcp=2500,cls=0.1,fcp=1800 # performance budget checkOutput includes TTFB, FCP, LCP, CLS, DOM Content Loaded, Page Load, resource count, and transfer size. The --budget flag checks each metric against a threshold and reports pass/fail.
browse security # full security audit
browse security --json # machine-readable outputAudits the current page for:
- Security headers: HSTS, CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy
- Cookie security: Secure, HttpOnly, SameSite flags
- Mixed content: HTTP resources loaded on HTTPS pages
browse extract table "table.results" # extract HTML table as text
browse extract table "table.data" --json # extract as JSON objects
browse extract table "table.data" --csv # extract as CSV
browse extract links # extract all links
browse extract links --filter "example" # filter links by pattern
browse extract meta # meta tags, Open Graph, JSON-LD
browse extract meta --json # machine-readable metadata
browse extract select "h2" # extract text of matching elements
browse extract select "a.nav" --attr href # extract attribute valuesbrowse viewport # show current viewport size
browse viewport 320 568 # set exact width x height
browse viewport 320x568 # alternative format
browse viewport --device "iPhone SE" # use a Playwright device profile
browse viewport --preset mobile # 375x667
browse viewport --preset tablet # 768x1024
browse viewport --preset desktop # 1440x900
browse responsive # screenshot at all default breakpoints
browse responsive --breakpoints 320x568,768x1024,1920x1080 # custom breakpointsDefine reusable flows in browse.config.json or as individual JSON files in a flows/ directory, then run them:
browse flow init smoke release-smoke
browse flow list
browse flow signup --var base_url=https://staging.example.com
browse flow signup --reporter junit # JUnit XML output for CI
browse flow signup --reporter junit --junit-property environment=staging
browse flow signup --reporter json # structured JSON output
browse flow signup --reporter teamcity # plugin-provided custom reporter
browse flow signup --dry-run # preview steps without running
browse assert text-contains "Welcome"
browse assert visible ".dashboard"
browse healthcheck --var base_url=https://staging.example.comGenerate starter Browse tests for an existing Vitest or Jest suite:
browse framework init vitest
browse framework init jest --dir qaThis creates a small browse-harness.cjs helper plus a runner-specific sample test that shells out to browse. Set BROWSE_BIN=./dist/browse if you want the generated tests to target a local build instead of a globally installed binary.
Run multiple named sessions within a shared Chromium process:
browse session create worker-1 # create a session (shared context)
browse session create worker-2 --isolated # create with isolated browser context
browse --session worker-1 goto https://a.com # route commands to a session
browse --session worker-2 goto https://b.com
browse session list # list all sessions
browse session close worker-1 # close a sessionBy default, sessions share the browser context (cookies, storage). Use --isolated to create a fully separate browser context with its own cookies, storage, and permissions.
Pool (library) for multi-agent orchestration:
import { createPool } from "browse/pool";
const pool = createPool({ socketPath: "/tmp/browse-daemon.sock", maxSessions: 10 });
const session = await pool.acquire();
await session.exec("goto", "https://example.com");
session.release();
await pool.destroy();browse intercept add "**/api/users" --body '{"users":[]}' # mock API response
browse intercept add "**/analytics/**" --status 204 # block with status
browse intercept list # list active rules
browse intercept remove "**/api/users" # remove a rule
browse intercept clear # remove all rulesBrowse defaults to Chromium but also supports Firefox and WebKit for cross-browser testing:
browse --browser firefox goto https://example.com
browse --browser webkit goto https://example.comOr via environment variable (must be set before the daemon starts):
BROWSE_BROWSER=firefox browse goto https://example.comNote: Stealth features are Chromium-specific and are not applied to Firefox or WebKit.
To install additional browsers, set BROWSE_BROWSERS before running setup:
BROWSE_BROWSERS="firefox webkit" ./setup.shRoute all browser traffic through an HTTP or SOCKS proxy:
browse --proxy http://proxy:8080 goto https://example.com
browse --proxy socks5://proxy:1080 goto https://example.comOr via environment variable:
BROWSE_PROXY=http://proxy:8080 browse goto https://example.comOr in browse.config.json (supports authentication and bypass lists):
{
"proxy": {
"server": "http://proxy:8080",
"bypass": "localhost,*.internal.com",
"username": "user",
"password": "pass"
}
}Note: The daemon must be restarted for proxy changes to take effect. Run
browse quitfirst.
Launch the browser visibly for debugging:
BROWSE_HEADED=1 browse goto https://example.comNote: Environment variable must be set before the daemon starts. If already running, run
browse quitfirst.
Extend browse with custom commands and lifecycle hooks. Plugins are TypeScript or JavaScript files that export a BrowsePlugin object.
// plugins/hello.ts
import type { BrowsePlugin } from "browse/plugin";
const plugin: BrowsePlugin = {
name: "hello",
version: "1.0.0",
commands: [{
name: "hello",
summary: "Say hello",
usage: "browse hello [name]",
handler: async (ctx) => ({
ok: true,
data: `Hello, ${ctx.args[0] ?? "world"}!`,
}),
}],
reporters: [{
name: "teamcity",
render: ({ flowName, results }) =>
`##teamcity[testSuiteFinished name='${flowName}' duration='${results.length}']`,
}],
};
export default plugin;Register in browse.config.json:
{
"plugins": ["./plugins/hello.ts"]
}Plugins can also hook into the command lifecycle (beforeCommand, afterCommand, cleanup), maintain per-session state, and register custom flow reporters that become available via browse flow --reporter <name> and browse test-matrix --reporter <name>. Place personal plugins in ~/.browse/plugins/ for auto-discovery across all projects.
Discover official starters and community plugins from the CLI:
browse plugins official
browse plugins search slack
browse plugins search notifications --limit 10Browse also ships official starter plugins for common integrations:
./examples/plugins/slack/index.ts./examples/plugins/discord/index.ts./examples/plugins/jira/index.ts
See the plugin authoring guide for full documentation.
Optional. Create browse.config.json in your project root to configure login environments, reusable flows, permission checks, and health checks.
{
"environments": {
"staging": {
"loginUrl": "https://staging.example.com/login",
"userEnvVar": "STAGING_USER",
"passEnvVar": "STAGING_PASS",
"usernameField": "Email",
"passwordField": "Password",
"submitButton": "Sign in",
"successCondition": { "urlContains": "/dashboard" }
}
},
"flows": {
"signup": {
"description": "Test the signup flow",
"variables": ["base_url", "test_email"],
"steps": [
{ "goto": "{{base_url}}/register" },
{ "fill": { "input[name=email]": "{{test_email}}" } },
{ "click": "button[type=submit]" },
{ "wait": { "urlContains": "/welcome" } },
{ "assert": { "textContains": "Welcome" } }
]
}
},
"timeout": 45000
}See configuration docs for full options including Playwright passthrough.
Architecture:
CLI ──JSON──▶ Unix socket ──▶ Daemon ──▶ Playwright ──▶ Chromium (default)
TCP socket ──┘ ├─▶ Firefox
└─▶ WebKit
The daemon spawns on first use and stays alive for 30 minutes of inactivity. It owns a single browser instance and communicates over a Unix socket. The CLI is a thin client that serialises commands as JSON. Named sessions allow multiple page groups to share one browser process.
The daemon socket is secured with a shared-secret authentication token stored at $XDG_STATE_HOME/browse/daemon.token. SIGTERM and SIGINT are trapped for graceful shutdown.
For remote agent access, the daemon can also listen on a TCP port via --listen <host>:<port>.
Stealth:
Browse ships with built-in anti-detection for headless Chrome — no plugins or extra config needed:
- Navigator patching — clean user-agent string, consistent
userAgentDatabrands/platform via CDP metadata - Screen spoofing — plausible monitor resolution and taskbar offset to avoid viewport-equals-screen detection
- Chrome stubs —
chrome.appandchrome.runtimestubs matching real Chrome - Worker coverage — user-agent and fingerprint patches in SharedWorker and ServiceWorker contexts
- Iframe protection — randomised mouse event coordinates to prevent CDP coordinate leaks (Cloudflare Turnstile)
Passes Sannysoft, Intoli, Pixelscan, and BrowserLeaks. Partially evades CreepJS.
Measured with browse benchmark:
| Command | p50 | p95 |
|---|---|---|
| goto | 27ms | 32ms |
| snapshot | 1ms | 11ms |
| screenshot | 24ms | 25ms |
| click | 17ms | 18ms |
| fill | 1ms | 26ms |
Target: p95 < 200ms for non-screenshot commands.
Browse has 90+ commands. Here are the most commonly used:
| Command | Description |
|---|---|
goto <url> |
Navigate to URL |
url |
Print current page URL |
snapshot |
List elements with refs (@e1, @e2...) |
click @eN |
Click element by ref |
fill @eN "value" |
Fill input (clears first) |
screenshot [path] |
Capture page |
a11y |
Accessibility audit |
perf |
Core Web Vitals |
security |
Security audit |
flow <name> |
Run configured flow |
flow init <template> |
Scaffold a built-in flow template |
framework init <runner> |
Scaffold Vitest/Jest Browse tests |
session create <name> |
Create named session |
status |
Daemon status (--json, --watch, --exit-code, --metrics) |
quit |
Stop the daemon |
See the full commands list for complete documentation including:
- DOM inspection (
html,title,attr,element-count) - Data extraction (
extract table\|links\|meta\|select) - Debugging (
console,network,trace,video) - Dialog handling (
dialog accept\|dismiss) - Iframes (
frame list\|switch) - Auth (
login,auth-state) - Visual regression (
vrt,diff) - CI/CD (
ci-init,test-matrix) - Test runners (
framework init vitest\|jest) - And more...
| Issue | Solution |
|---|---|
| Changes not applying | Run browse quit to restart the daemon after binary rebuilds |
| Proxy not working | Daemon must be restarted for proxy changes: browse quit then retry |
| macOS "cannot verify developer" | Run xattr -d com.apple.quarantine ~/.local/bin/browse |
| Stealth not working | Ensure you're using Chromium (default). Stealth doesn't apply to Firefox/WebKit |
| Refs stale | Run browse snapshot again after navigation — refs are page-specific |
| Session/auth issues | Run browse wipe to clear all cookies and storage |
| Port already in use | Check for zombie daemon: lsof -i :<port> or browse quit |
| Need machine-readable health metrics | Use browse status --metrics for Prometheus format or browse status --json for structured health output |
| Need richer daemon logs | Set BROWSE_LOG_FORMAT=json and optional BROWSE_LOG_LEVEL=debug|info|warn|error |
| Long-running daemon using too much memory | Set BROWSE_MAX_RSS_MB=<limit> to trigger memory-pressure mitigation; tune slow command profiling with BROWSE_SLOW_COMMAND_MS=<ms> |
MIT — see LICENSE. Copyright (c) 2026 Forjd.dev
Questions or issues? Open an issue or start a discussion.