cd /news/ai-agents/how-browseract-fixed-the-stale-selec… · home topics ai-agents article
[ARTICLE · art-81775] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

How BrowserAct Fixed the Stale-Selector Failures Breaking My Browser Tasks

BrowserAct, a browser automation platform for AI agents, addresses the stale-selector problem that breaks browser tasks when pages re-render with new build hashes. In a test against raw Playwright, BrowserAct's state-based indexing, which uses snapshot-scoped indices instead of selectors, proved resilient to id and class churn. The platform's approach allows agents to reason about the page state turn by turn and detect when the page has changed.

read6 min views1 publishedJul 31, 2026

Disclosure: BrowserAct sponsored this piece. The BrowserAct links below are affiliate-tracked — I get credit if you sign up through them.

I keep hitting the same failure building agent workflows in Claude Code: the agent captures a selector once, the page re-renders with a new build hash, and the next run breaks on an element that's still right there on the screen — just under a different id. BrowserAct is a browser automation platform built for AI agents — real browser control, persistent browser identity, task sessions, verification handling, and human handoff, all behind one CLI. This test is about one narrow slice of that: how it handles a page whose ids and classes change under you, using Claude Code to drive it.

I tested that against a page I built myself, compared directly against raw Playwright.

Here's the shape of it. A frontend re-renders with fresh CSS-module or styled-components hashes on every deploy. You write:

const id = await page.locator("button").getAttribute("id");
await page.reload();
await page.locator(`#${id}`).click({ timeout: 3000 });

That works the day you write it. It breaks the next time the build hash changes, and the failure you get is a bare timeout with no explanation:

locator.click: Timeout 3000ms exceeded.

Playwright's own role/text locators fix this specific case — getByRole("button", { name: "Submit" })

survives id/class churn fine, because it never depended on the hash in the first place. That's a real fix, not a workaround. What it doesn't give you is a representation of the page an agent can reason about turn by turn, or a signal that says "the page under you just changed, stop and re-check." That's the gap BrowserAct is actually filling.

BrowserAct doesn't hand Claude Code a selector at all. The loop is: read the current state, choose an action from what's actually there, execute it, reassess.

state  -> indexed list of interactive elements, as of right now
click <index>  -> act on one of them
state  -> read again, because the page may have changed

The index is scoped to that one snapshot. It's not a selector, not a stable id — it's a pointer into "what state just returned," and it expires the moment the page does something a new state

call would notice.

uv tool install browser-act-cli --python 3.12
browser-act --version
browser-act browser create --name "dom-drift-test" --type chrome --desc "local churn test"
browser-act browser list

browser list

after creation is how you get the browser id you'll pass to every session command — it's not returned anywhere else. I used the chrome

browser type (a local, blank browser, immediate to create) rather than stealth

, since this test is about selector churn on a page I control, not anti-bot evasion — stealth

requires an API key and a billed purchase flow that has nothing to do with what I was testing. Creating a local chrome

browser completed immediately with no purchase page involved.

Versions tested: browser-act-cli

v1.1.0, Playwright 1.61.1, Python 3.12.13. To reproduce this: the test page is a small Node server that reshuffles element ids, classes, and order on every reload — no third-party site involved — plus the Playwright scripts run against the same page.

Skill source: browser-act/skills. Installation details: docs/installation.md.

browser-act --session domtest browser open <browser-id> http://localhost:8934
browser-act --session domtest state

state

returns an indexed list, not a selector:

[1]<button class=item-r8k7gx invalid=false />
        Bravo
[2]<button class=item-cr2ajb invalid=false />
        Charlie
[5]<button id=submit-btn-ckeewz invalid=false />
        Submit

You act on the index (click 5

), not the class or id, so the build-hash problem doesn't apply to it — there's no hash in the reference to go stale.

I built a local test page that reshuffles element ids and item order on every reload, specifically to force this failure. Playwright's role locators, as noted above, survive that fine. The difference isn't that Playwright can't handle churn — it's what happens when you act on a reference that's gone stale anyway, whether from a captured selector or an old snapshot:

Playwright, selector captured once, reused after reload:

FAILED clicking #submit-btn-g9mpcm:
  locator.click: Timeout 3000ms exceeded.

BrowserAct, index captured once, reused after reload:

Error 210603: The snapshot belongs to a different page or tab than the
current one. Run 'browser-act state' again on the current page.

Both refuse to silently click the wrong thing. But BrowserAct's error names the exact cause and the exact fix — re-run state

— where Playwright's is a generic timeout you already have to know how to interpret.

That error is only useful if what follows it actually works. Here's the full loop, one continuous session, real output, no steps skipped — the error and the successful recovery from it, back to back:

browser-act --session shot1 browser open <browser-id> http://localhost:8934
browser-act --session shot1 state
[5]<button id=submit-btn-5zhwmn invalid=false />
        Submit
load 1

Page changes, and the old reference is used anyway — the error from earlier, live:

browser-act --session shot1 reload
browser-act --session shot1 click 5
Error 210603: The snapshot belongs to a different page or tab than the
current one. Run 'browser-act state' again on the current page.

Re-check, don't retry blind:

browser-act --session shot1 state
[5]<button id=submit-btn-c66evj invalid=false />
        Submit
load 3

The id changed (submit-btn-5zhwmn

to submit-btn-c66evj

), and the fresh state

call caught it. Acting on the new index closes the loop:

browser-act --session shot1 click 5
clicked=5

State, act, hit the error, reassess, act again — completed, in the same session the error happened in.

It reduced my dependence on generated ids and CSS classes to zero for anything clickable, and gave Claude Code a predictable recovery path the moment the page changed underneath it: re-run state

, act on what's actually there now.

Two real limits, not softened. state

only indexes interactive elements — in my test, the list items only got indices once I made them buttons. For plain text or document content, the right tool isn't state

at all, it's BrowserAct's content-extraction commands (get markdown

, get text <index>

) — treating state

as a universal page parser is the wrong mental model. And separately: the title

field in state

's own output lagged behind the actual page content in one of my runs — the visible elements had already changed, title

hadn't caught up yet. Worth knowing if you're tempted to key any check off title

specifically.

BrowserAct solved the selector problem. It didn't remove the need to think about website authentication separately. Three different things are in play here, and it's worth naming them precisely: the browser identity (the chrome

browser instance itself), the BrowserAct task session (--session domtest

), and the target website's own authentication session.

In an earlier run against the same test page, the website authentication state expired while the BrowserAct task session remained active — state

kept responding normally, it just started describing a "please sign in again" page instead of the dashboard. BrowserAct exposed that changed page state clearly enough for Claude Code to decide what to do next: continue, re-authenticate, or hand off to a human. It didn't decide that automatically, and I don't think it should — that's still a call I want made explicitly, not inferred.

Claude Code and BrowserAct solved the actual failure mode I set out to test: brittle selector maintenance replaced with a workflow built on current page state, indexed actions, and an explicit recovery step when the ground shifts. That's a narrower claim than "browser automation solved," and it's the one I can actually stand behind.

Try BrowserAct: browseract.ai/Daniel

BrowserAct Skills: github.com/browser-act/skills

(Both links above are affiliate-tracked to my account.)

── more in #ai-agents 4 stories · sorted by recency
── more on @browseract 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/how-browseract-fixed…] indexed:0 read:6min 2026-07-31 ·