The screenshot loop sends your frontier model a ~1,600-token image to find one button, round-trips every click through it, and still loses the thread. bx is a CLI over real Chrome built for exactly one user: a coding agent. Distilled text instead of pixels, hard budgets at the source, a cheap model doing the driving — and a path that works becomes a typed flow that never needs a model again.
$ bun add -g bx-browser$ bx install-skillSame two lines on macOS, Linux, and Windows. Requires Bun and Chrome. The second line drops the Claude Code skill into ~/.claude/skills/bx — then tell Claude Code: “use bx to verify your changes.”
Runs on your Claude Code subscription. Delegated drives go through the Agent SDK with the same auth Claude Code itself uses — Haiku by default, escalating to Sonnet, Opus if you allow it. Signed in with a Max plan, that's your subscription: no API key, no separate bill. What it saves you is time, context, and wasted runs.
One honest caveat: if ANTHROPIC_API_KEY is set in your environment, Claude Code — and therefore bx — bills the API instead. That's how Claude Code's auth resolution works, not a bx decision; unset the variable and you're back on the subscription.
how you use it
Install the skill once and Claude Code reaches for bx on its own whenever a browser task shows up. Real prompts, and the commands they turn into:
you say
“Use bx to verify your changes.”
claude code runs
$ bx open https://myapp.localhost$ bx expect text "Saved"$ bx console$ bx net --failedThe default verification trio: an assertion, console errors, failed requests. expect exits 1 on failure — the agent gets a hard signal, not a screenshot to squint at.
you say
“Test the login flow with bx and keep it as a regression.”
claude code runs
$ bx agent "log in as demo@taskbox.test and verify the task list loads" \ --save loginHaiku drives on your Claude Code subscription — escalating to Sonnet (and Opus, if you allow it) only when it has to. Your session gets a ~300-token report, and flows/login.flow.ts replays forever at zero model tokens.
you say
“Fill out the signup form and tell me what the error toast says.”
claude code runs
$ bx els --filter signup$ bx fill "Email" "qa@test.dev"$ bx click "Create account"$ bx text ".toast"Hand-driving, for when each intermediate state matters. Targets are text or selectors, every read is budgeted, and nothing here costs an image.
you say
“Record a walkthrough of the checkout bug for the PR.”
claude code runs
$ bx record start checkout-bug$ bx run flows/checkout.flow.ts --record$ bx record stopProduces recordings/checkout-bug/report.md — deduped keyframes, contact sheets, narrated by bx's own action log. Drop it straight into the PR.
the entire integration
bx install-skill writes a ~130-line skill file to ~/.claude/skills/bx. It covers every verb and, more importantly, when to reach for them: "verify in the browser", "check the console", "test the game", any local dev URL. It loads 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 touched.
side by side
This is why bx exists: nobody wants the screenshot loop, and there is nothing the extension can do about it. 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.
Fable — the main session


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




#/login
idle — press play
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
Most people run this on a Claude Code subscription, so the spend that matters isn't dollars — it's minutes, context, and wasted runs.
seconds to a verified result
Log in and toggle a setting — the race above, unedited. The extension spent 3.2 minutes and never landed the navigation.
tokens per observation
bx els is the numbered element list the model acts on — budgeted at the source, so a look at the page is never a context event.
tokens into your main context, same task
Every screenshot and retry the extension takes lands in your context and stays there. bx hands back a verdict; the drive stays on Haiku.
under the hood
What actually happens when Claude Code hands a browser task to bx agent — every artifact below is real captured output.
in what claude code runs — you never see this either
$ bx agent "log in as demo@taskbox.test, turn on dark mode in Settings, and verify it stuck" --save dark-modeOne instruction, phrased by Claude Code from whatever you said. No element refs, no coordinates, no screenshots on your side of the wire.
what haiku reads each turn
✓ TaskBox — http://127.0.0.1:57395/fixture
[1] textbox "Email"
[2] textbox "Password"
[3] button "Sign in"Its entire view of the page — ~42 tokens. No screenshot on either side of the wire.
during what Haiku did — scroll it, every turn is here
14 turns · 25.9s · stays on Haiku · on your Claude Code subscription
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.9sThe report — roughly 300 tokens. The 14-turn transcript on the right never enters your session.
// synthesized by bx agent — flows/dark-mode.flow.ts
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");
});This is a flow — a typed TypeScript file Haiku wrote from the actions that actually worked. tsc checks it, git versions it, and bx run flows/dark-mode.flow.ts replays it tomorrow in 1.2s with zero model involvement. The agent run is the authoring cost; every run after is a free regression test.
measured on a real production app
| task | turns | wall |
|---|---|---|
| holidays count | 11 | 26s |
| overview + all-tasks synthesis | 37 | 75s |
| users + weekend schedule | 18 | 23s |
| module types | 11 | 16s |
| tags | 13 | 20s |
| settings profile | 13 | 12s |
| holidays + PTO | 22 | 30s |
7/7 correct, independently verified. 5 agents ran concurrently against one browser — each pinned to its own tab. On a Claude Code subscription the spend that matters is the one above: turns and seconds, not dollars.
Your context pays for the verdict, not the drive.
how it works
A daemon, a set of verbs, and files you can commit.
01
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
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
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.
real-time mode
A read→decide→click loop is seconds per decision; a game runs at 60fps. So bx drive installs an agent-authored controller that runs inside the page every frame, while the daemon polls a win condition — zero model tokens while it plays. Games, drag physics, anything the DOM can't describe.
recordings
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.
tradeoffs
bx is not a superset. Three cases where it is the wrong choice:
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. (A bridge extension into your daily Chrome is the v2 roadmap.)
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.
bx is built for driving apps you're actually working on — a dev server, a real project, a flow file worth keeping. For one-off poking at a site you have never seen, the screenshot loop needs no setup and no profile.