The Browser Extension Was Eating 20% of My Tokens, So We Built a CLI
Claude (Fable 5)
This post was written by Claude (Fable 5), which also designed and built the tool it describes. dested prompted, made the product decisions, and edited. The first person below is dested's voice.
Most of my development runs through Claude Code, and a lot of it ends in a browser: does the component render, does the flow click through, is the bug fixed. For months the answer was the Claude in Chrome extension — an MCP server that lets the model drive a real Chrome tab.
It works, but at one point it was around 20% of my total token usage, and
the model doing the clicking was Opus or Fable — my most expensive context,
spent on pressing buttons. So we replaced it with
bx, a purpose-built browser CLI
for Claude Code. This is the measured comparison.
Why the extension is expensive
Four architectural choices compound:
- The expensive model sits in the per-action loop. Screenshot in → model reasons → coordinates out → screenshot to confirm. The driver is your main session, billed accordingly.
- Observation is vision-first. A 1568×776 screenshot costs ~1,620 image tokens, arrives constantly, and stays in context for the rest of the session. Fifteen steps ≈ 25k tokens of screenshots, resident forever.
- Text fallbacks are unbounded.
read_pagedumps 5–30k tokens on a production app; console and network readers don't cap unless you remember to filter. - Nothing is reused. The same login flow costs full price every session. There is no artifact.
Plus ~24 tool schemas riding along in every context.
The inversion
bx starts from one assumption: I'm driving my own apps on localhost, not
arbitrary websites. The DOM is mine, the same flows repeat daily, and the
expensive model doesn't need to drive — it needs to read a report.
- CLI, not MCP. A verb is a Bash call (
bx click "Save", ~10 tokens). A ~90-line skill replaces 24 resident schemas. - Text-first, budgeted at the source.
bx elsprints numbered interactive elements —[3] button "Save changes"— hard-capped at ~800 tokens by the daemon. Screenshots are deliberate, downscaled, returned as a path. - Deterministic replay. Flows are typed TypeScript files.
bx run flows/login.flow.tscosts zero model tokens and doubles as a regression test with an exit code. - The cheap model drives.
bx agent "log in and verify the invoice list loads"runs a Claude Agent SDK session on Haiku (your existing Claude Code subscription), escalating once to Sonnet on failure. Your main session gets a ~300-token report.
Architecture: a per-profile daemon (Node + playwright-core) holds your real installed Chrome; CLI, flows, and agent talk to it over localhost HTTP; Playwright locators do the clicking, so actions auto-wait instead of screenshot-guess-retry.
Measurements
Deterministic numbers are from bx bench (in the repo). Old-way numbers are
from driving the identical task through the Claude in Chrome MCP tools in a
live session. The task, both times: open a small test app, log in, navigate to
Settings, toggle dark mode, verify "Dark mode on."
The two sessions, replayed
Below are the two real transcripts — the left captured live through the Claude in Chrome MCP tools, the right a bx agent run on Haiku — replayed on a shared clock.
Both transcripts are real and unedited in sequence; results are truncated for display. Totals differ slightly from the tables above because every run varies.
Cost of looking at a page
| One observation | Tokens |
|---|---|
bx els (distilled elements) |
42 |
read_page, tiny fixture (old way's best case) |
~120 |
| One screenshot, 1568×776 | ~1,620 |
| Raw DOM dump of the same page | ~2,564 |
read_page, production app (typical) |
5,000–30,000 |
The element list grows with interactive elements, not DOM size or pixels, so the gap widens on real apps.
Cost of the whole task
| Claude in Chrome (live) | bx agent |
bx run replay |
|
|---|---|---|---|
| Driver | Fable/Opus (main context) | Haiku (subagent) | none |
| Outcome | failed — see below | pass, no escalation | pass |
| Wall clock | ~3.5 min (incl. 60s of timeouts) | 21.0s | 1.2s |
| Tokens | 8,000+ premium | 32,499 Haiku | 0 |
| API-equivalent cost | ~$0.08–0.20 + reasoning | $0.037 | $0.000 |
| Main-session context | all of it, retained | ~300-token report | one PASS line |
| Second run tomorrow | full price again | full price again | 0 tokens, 1.2s |
The last row is the point. The agent run is the authoring cost: bx agent --save login synthesizes the actions taken into a typed flow with
replay-stable targets — testids and accessible names, never coordinates — and
the flow is free from then on.
Haiku went 19-for-19 on turns with no escalation. Given a numbered element list and twelve verbs, browser driving is translation, not reasoning. The model that decides whether to click Settings now spends ~300 tokens instead of 30,000.
Reliability
The live old-way session, on a static page with five buttons:
- Two
Page.captureScreenshotCDP calls timed out at 30 seconds each. - A click reported success and silently did nothing — discovered only by spending another ~1,620-token screenshot.
- Screenshots came back 1568px wide against a 1512px viewport — a 3.7% coordinate scale mismatch, so clicks computed from screenshot pixels land off-target.
- After the first timeout, input delivery degraded and never recovered. Three attempts at the same navigation all "succeeded" without navigating.
It failed the task on a static HTML page written to be easy.
bx has no coordinates anywhere in the system. Targets are Playwright
locators — testid, then role+name, then text — with auto-waiting. When a
target doesn't resolve, the error includes did-you-mean candidates from the
live DOM, which is how a Haiku-class model self-corrects without a
screenshot. The 10-test smoke suite (real daemon, real Chrome, login, refs,
console, network, flow replay) runs in 4.6 seconds.
Speed
| Measured | |
|---|---|
| Cold start (spawn daemon + launch Chrome + load page) | 730 ms |
bx els |
2.3 ms |
bx click (incl. navigation + settle) |
187 ms |
bx snap (downscaled screenshot to disk) |
62 ms |
| 9-step flow replay, end to end | 1,190 ms |
Every number includes the full HTTP round trip through the daemon. One old-way screenshot response costs more wall time than an entire flow replay.
Recordings that narrate themselves
bx record start bug-repro … drive … bx record stop. Chrome records video;
bx pipes the webm through
video-to-prompt — running inside
the browser it already controls, since the library is browser-only — and
writes an agent-ready package: deduped keyframes, 3×3 contact sheets, and a
report.md built for LLM reading.
There's no audio narration, but bx knows what it did and when. The action log
— "clicked Save changes", "filled Email" — becomes the video's transcript,
timed to the millisecond, passwords redacted before they touch disk. The
report reads as a narrated walkthrough. Verified end to end: record stop
flushed the webm, distilled it in-browser, and wrote the full package in 3.4
seconds.
Caveats
- The fixture is the old way's best case. A tiny static page keeps
read_pagecheap; production dumps are 40–250× bigger. bx's element list grows only with interactive elements. - One session, not a failure-rate study. Two CDP timeouts in fourteen calls could be a bad day. The coordinate-scale mismatch is structural.
- Authoring isn't free. The first pass at a flow costs a Haiku run (~$0.04-equivalent) or your own attention. Amortization starts on run two.
- bx only drives apps you can reach. It launches your real Chrome with persistent profiles, but the daily-driver profile you already have open is out of reach — Chrome 136+ blocks CDP on the default user-data-dir. A playwright-crx bridge extension is on the roadmap.
- Claude in Chrome is more general. Zero setup on any site, and it can lean on a frontier model's visual judgment. For a site you don't control, hostile to automation, visited once — it's still the tool.
Closing
bx was built by Claude agents working in parallel from fully-specified specs, with the incumbent's failure modes measured live alongside. The build surfaced two real platform bugs: Bun can't host Playwright's CDP pipe on Windows, and detached process spawning breaks under Bun's job objects — the daemon runs under Node because of them.
The conclusion is the architecture, not the tooling drama: for your own apps, browser automation is a plumbing problem, not a perception problem. Expose the page as cheap text, make actions deterministic, cache what works as code, and save the expensive model for decisions. My browser bill went from 20% of my tokens to a rounding error, and the replacement runs my regression flows for free.