bx

Browser automation for Claude Code — text-first, token-budgeted, and the cheap model drives.

A CLI over real Chrome that replaces the screenshot loop. The model reads a distilled element list instead of a 1,620-token image, issues one-line commands, and once a path works it becomes a typed flow file that never needs a model again.

install
$ git clone https://github.com/dested/claude-browser$ cd claude-browser$ bun install$ bun link

Requires Chrome and Bun. bun link puts bx on your PATH.

what it looks like

Four things you do every day

Real commands, real output. Pick one.

Inspect a page

Pick a task above — the output is what the CLI actually prints.

the artifacts

No magic

The entire interface between the model and the browser is plain text. Here it is.

What the model reads

✓ TaskBox — http://127.0.0.1:57395/fixture

[1] textbox "Email"
[2] textbox "Password"
[3] button "Sign in"

This is the model's entire view of the page — no screenshot, no DOM dump. ~42 tokens.

the payoff

Agent mode

One instruction in. A verdict out. The drive happens on a model that costs 1/10th of yours.

in what you type

$ bx agent "log in as demo@taskbox.test, turn on dark mode in Settings, and verify it stuck" --save dark-mode

One instruction. No element refs, no coordinates, no screenshots on your side of the wire.

during what Haiku did (you never see this)

I'll help you with this task. Let me start by opening the URL and then proceed with loggin…
$ bx open http://127.0.0.1:62747/fixture
TaskBox — http://127.0.0.1:62747/fixture
Good! I'm on the login page. Now I'll log in with the provided credentials.
$ bx fill 1 demo@taskbox.test
$ bx fill 2 hunter2
ok — filled 1 with "demo@taskbox.test"
ok — filled 2 with "hunter2"
Now I'll click the Sign in button.
$ bx click 3
ok — clicked 3
Good! I've logged in successfully. Now I need to go to Settings. Let me see what elements …
$ bx els
[1] link "Tasks"
Perfect! I can see the Settings link. Let me click on it.
$ bx click 2
ok — clicked 2
Great! I'm now on the Settings page. Let me check what elements are available to find the …
$ bx els
[1] link "Tasks"
Excellent! I can see the Dark mode checkbox. Let me click on it to enable dark mode.
$ bx click 4
ok — clicked 4
Now I need to save the changes. Let me click the Save button.
$ bx click 5
ok — clicked 5
Let me verify that the dark mode has been enabled by checking the page text for "Dark mode…
$ bx expect text Dark mode on
PASS: text "Dark mode on" is visible
Perfect! Let me take a screenshot to confirm the state and then report success.
$ bx snap
screenshot saved: C:\Users\dested\.bx\snaps\20260806-182200.png
Excellent! I have successfully completed all the requested tasks:
$ bx report pass I opened the TaskBox login page at http://127.0.0.1:627…
report recorded — you are done, stop now.
Task completed successfully! Here's what I did:

14 turns · 25.9s · stays on Haiku

out what your context receives

PASS — Logged in, enabled dark mode in Settings, verified.
Evidence: expect text "Dark mode on" ✓ · final url #/settings
tier=haiku turns=14 wall=25.9s tokens=31551/948 cost=$0.062

The report — roughly 300 tokens. The 14-turn transcript above never enters your session.

import { flow } from "bx/flow";

export default flow("dark-mode", async (b) => {
  await b.open("http://127.0.0.1:8000/fixture");
  await b.fill("Email", "demo@taskbox.test");
  await b.fill("Password", "");  // redacted at record time
  await b.click("Sign in");
  await b.click("Settings");
  await b.click("Dark mode");
  await b.expectText("Dark mode on");
});

Run it tomorrow: bx run flows/dark-mode.flow.ts — 1.2s, zero model tokens. The agent run is the authoring cost; every run after is free.

measured on a real production app

taskturnswallcost
holidays count1126s$0.025
overview + all-tasks synthesis3775s$0.061
users + weekend schedule1823s$0.034
module types1116s$0.028
tags1320s$0.029
settings profile1312s$0.028
holidays + PTO2230s$0.035

7/7 correct, independently verified. 5 agents ran concurrently against one browser — each pinned to its own tab.

Your context pays for the verdict, not the drive.

the race

The same task, two drivers, one clock

Log in and toggle a setting on a fixture app. Left is a real Claude in Chrome session. Right is a real bx agent run. Neither timeline has been edited.

The fixture is TaskBox — a ~380-line static demo app that ships in the repo (login → tasks → settings), so the bench is reproducible: fixtures/app/index.html. The screenshots below are bx snap output.

t = 0.0s

Claude in Chrome

0 tok

Fable — the main session

TaskBox login screenTaskBox tasks screen

#/login

$ tabs_context()
running — 0.0s

bx agent

0 tok

Haiku — a subagent; the main session reads a ~300-token report

TaskBox login screenTaskBox tasks screenTaskBox settings screenTaskBox settings dark screen

#/login

idle — press play

running — 0.0s

Both timelines are real sessions against the same fixture app, replayed on one clock. Left: the extension, driven by the main session — coordinate clicks, two 30-second CDP timeouts, and a navigation that never landed. Right: bx agent on Haiku, finishing while the left pane is still waiting.

measured

What that buys you

One observation

42tokens

vs 1,620 one screenshot · 2,564 raw DOM dump

bx els — the numbered element list the model actually acts on.

A delegated task

$0.037

Haiku drives end to end. Across seven real tasks on a production app: $0.025–$0.061, 12–75s, 7/7 correct.

A saved flow, replayed

1.2s· 0 tokens

Typed TypeScript, checked by tsc, in version control. Re-running costs no model at all.

Cold start

730ms

Daemon up and Chrome attached. An els scan is 2.3ms, a click 187ms after that.

  • 4× more interactive elements surfaced than the extension's tools
  • ~18× more page text per read
  • 5 agents driving one browser concurrently

how it works

Three moving parts

A daemon, a set of verbs, and files you can commit.

01

A daemon holds your real Chrome

One background process per profile owns a real Chrome instance over playwright-core — persistent user-data-dirs, so you sign in by hand once and the session survives every run. The CLI talks to it over localhost HTTP. Nothing is a bundled headless browser unless you ask for one.

02

Verbs act on distilled text

open, els, click, fill, expect, console, net. Every read is budgeted at the source — the element list caps around 800 tokens, page text at 2,000, console and network at 30 entries — so output can never blow up a context window, and the model never has to ration a look at the page.

03

Flows replay for free

A path that works becomes a typed TypeScript file. tsc checks it, git versions it, and re-running it costs zero model tokens — which makes the same artifact a regression test you can drop into CI or a pre-push hook.

agent mode

The cheap model does the clicking

bx agent takes a natural-language task, runs it on Haiku through the same verbs, and escalates once to Sonnet if the first attempt fails. What comes back to your main session is a ~300-token report — pass/fail, a summary, evidence lines, turns and usage — not a transcript. Add --save and the actions taken are synthesized into a flow file with replay-stable targets.

recordings

Video becomes something a model can read

bx record start captures Chrome's own video, then distills it into deduped keyframes, 3×3 contact sheets, and a report.md. There is no audio track, so bx's own action log — clicked Save changes, filled Email — becomes the timed transcript, and the report reads as a narrated walkthrough.

the whole prompt surface

The model learns all of this from one file

Everything above is taught by a single ~130-line skill file — skill/SKILL.md — and it is loaded only when a browser task actually shows up. Compare that to ~24 MCP tool schemas sitting resident in every context you ever open, whether or not the browser is ever touched.

tradeoffs

When the extension is still the right tool

bx is not a superset. Three cases where it is the wrong choice:

  • Your own logged-in daily browser

    bx drives its own profiles under ~/.bx/profiles. Chrome 136+ blocks CDP on the default user-data-dir and app-bound encryption blocks importing cookies, so the browser you already have open, with all its sessions, stays out of reach. The extension lives inside it.

  • Hierarchical accessibility-tree reads

    els returns a flat, numbered list tuned for acting on things. When the question is about structure — nesting, landmarks, what contains what — read_page's tree is genuinely the better read.

  • Arbitrary third-party sites

    bx is built for driving apps you own, where testids and stable accessible names exist and a flow file is worth keeping. For one-off poking at a site you have never seen, the screenshot loop needs no setup and no profile.