browser_find and the Token Diet: Cheaper Playwright MCP Runs with Claude Code

I was watching Claude Code drive a checkout flow through the Playwright MCP the other day, and I finally understood where my context window goes to die.

The task was trivial: add an item to the cart, go to checkout, click Place order. Four clicks. But every single step, before the agent could touch a button, it called browser_snapshot — and got back the entire accessibility tree of the page. Header, nav, product grid, footer, cookie banner, the works. Then it clicked, the page changed, and it snapshotted the whole thing again. And again. By the third step the model was re-reading a few thousand tokens of aria-tree just to find one button it had already seen.

That's the part nobody budgets for. It's not the "write me a test" prompt that's expensive — it's the loop. And on the current weekly-limit plans, that loop is exactly what pushes you into the ceiling halfway through a suite.

The good news: the Playwright MCP shipped the tools to fix this. Let me show you what was actually eating the tokens, and the two-line change that put the runs on a diet.

The problem: snapshot-per-step is the default, and snapshots are big

The Playwright MCP's whole model is the accessibility snapshot. It's genuinely a good idea — instead of feeding the model screenshots (expensive, imprecise) or raw HTML (huge, noisy), browser_snapshot returns a structured accessibility tree: roles, names, and a ref for each element the agent can act on. The docs literally say it's "better than screenshot," and they're right.

The catch is scale. A snapshot is the whole page. On a marketing landing page that's manageable. On a real app — a dashboard, a data table, a checkout with a populated cart — the tree runs long. And the agent doesn't snapshot once; it snapshots after every navigation and often after every click, because the page changed and it needs the new refs.

So your cost is roughly:

snapshot_tokens  ×  (steps + retries)  ×  (however many times the model re-reads it)

None of those factors is small in a real E2E flow. This is the same pressure behind the HN thread everyone was quoting this week — "Claude Code sends 33k tokens before reading the prompt" — except here it's self-inflicted, one snapshot at a time, and it compounds.

The fix, in three moves

1. Stop dumping the whole tree — search it instead

Playwright MCP v0.0.78 (2026-07-09) added browser_find. Straight from the release notes:

browser_find — Search the accessibility snapshot of the current page for text or a regular expression. Returns matching snapshot nodes with a few lines of surrounding context (like search snippets), which is cheaper than capturing the whole snapshot when you only need to locate an element and its ref.

That last clause is the whole point. When you already know what you're looking for — a button labeled "Place order", a row containing an order number, an error toast — you don't need the entire tree. You need that node and its ref. browser_find gives you the node plus a few lines of context, like grep on the page instead of cat.

The parameters are simple (from the tool reference):

  • text"Plain text to search for in the page snapshot (case-insensitive substring match)."
  • regex"Regular expression to search for in the page snapshot. Matching is case-sensitive by default; wrap the pattern in slashes to add flags, e.g. /error/i for case-insensitive."
  • Provide either text or regex, not both.

2. Cap the blast radius of any single response

Even a snapshot you do need can blow up. v0.0.76 (2026-06-10) added --output-max-size — a threshold (in bytes) that caps the size of tool responses, with post-response disk eviction of oversized output. You set it once when you launch the MCP server:

// .mcp.json
{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest", "--output-max-size", "20000"]
    }
  }
}

Think of it as a seatbelt, not a strategy: it stops one runaway tool call from swallowing your whole context. It does not make your snapshots smart — it just truncates. (More on why that matters in the limitations.)

3. Tell the agent to reach for find first

New tools don't help if the model defaults to old habits. Put the policy where Claude Code actually reads it — a short, test-specific note in CLAUDE.md:

## Playwright MCP conventions
- To locate a known element, use `browser_find` (text or regex), not `browser_snapshot`.
- Only call `browser_snapshot` when exploring an unfamiliar page or when `browser_find` returns nothing.
- Prefer acting on the `ref` returned by `browser_find` directly.

Keep it that short. A bloated memory file has its own token cost — but that's a different post.

A real example: the same checkout flow, two ways

Here's the flow I started with. Same prompts, same page, two strategies.

The old way — let the agent snapshot per step:

> Add the first product to the cart, go to checkout, and click "Place order".
> Verify the confirmation shows an order number.

Under the hood, per step, you get responses like this — and this is the trimmed version:

- generic [ref=e1]:
  - banner [ref=e2]:
    - link "Home" [ref=e3]
    - navigation [ref=e4]:
      - link "Products" [ref=e5]
      - link "Deals" [ref=e6]
      - link "Account" [ref=e7]
    - button "Cart (0)" [ref=e8]
  - main [ref=e9]:
    - heading "All products" [level=1] [ref=e10]
    - list [ref=e11]:
      - listitem [ref=e12]:
        - heading "Wireless Mouse" [ref=e13]
        - button "Add to cart" [ref=e14]
      # … 40 more list items …
  - contentinfo [ref=e220]:
    # … footer links, cookie banner …

Multiply that by four steps plus a retry, and you've spent real budget re-reading furniture the agent doesn't care about.

The find way — locate, then act:

> Use browser_find to locate the "Place order" button, then click its ref.
> Then browser_find the confirmation text matching /order #\d+/ and report it.

The browser_find for "Place order" comes back as a snippet, not a tree:

# browser_find text="Place order"
- button "Place order" [ref=e63]
  ↑ region "Order summary" [ref=e60]
  ↓ paragraph "By placing this order you agree to the Terms" [ref=e64]

That's the element, its ref, and enough context to know it's the right one — a few lines instead of a few hundred. The agent clicks e63 and moves on. For the assertion, regex="/order #\\d+/" pulls back just the confirmation node instead of re-snapshotting the whole success page.

On the savings: a full snapshot of a populated app page commonly runs on the order of a few thousand tokens; a browser_find snippet is tens to low-hundreds. Across a multi-step flow with retries, that's the difference between comfortably staying under your limit and hitting it mid-suite. I'm giving you a range, not a benchmark — the honest move is to measure your own pages (watch the token counter, or diff two runs), because it depends entirely on how heavy your DOM is.

Where this bites you

I'd be lying if I said find is a free win everywhere. Things I've tripped on:

  • You have to know what to search for. browser_find is for locating, not exploring. The first time the agent lands on an unfamiliar page, it still needs one browser_snapshot to learn the layout. find shines on the second visit and beyond.
  • The regex is case-sensitive by default. regex="/Error/" will silently miss error. Use text for plain matches (it's case-insensitive), or add the /i flag deliberately. And you can't pass text and regex together — pick one.
  • --output-max-size truncates; it doesn't summarize. If the thing you needed was in the part that got evicted, the agent won't know it's missing — it'll just work with less. Set the cap generously and treat it as a backstop, not your primary savings.
  • Accessibility-tree only. Canvas-rendered UIs, <iframe> soup, and un-labeled <div> buttons won't show up cleanly in either tool. That's an app-accessibility problem, and no MCP flag fixes it.
  • Numbers vary wildly. As I said above — I didn't run a controlled benchmark. Don't quote my range as gospel; instrument your own suite.

Takeaways

  • The expensive part of agent-driven browser testing isn't the prompt — it's browser_snapshot running on every step of the loop.
  • browser_find (v0.0.78) turns "read the whole page" into "grep the page": pass text or regex, get the matching node and its ref, act on it.
  • --output-max-size (v0.0.76) is your seatbelt against a single runaway response.
  • A three-line CLAUDE.md convention is what actually changes the agent's behavior — the tool existing isn't enough.
  • Measure your own pages. My ranges are a starting intuition, not a number to put in a report.

Next up in this series I'll dig into the flip side: what a full Claude Code test-suite run actually costs once you add up the fixed per-prompt overhead and the weekly limits — and how to budget for it before it budgets for you.

Last Update: July 12, 2026