{"slug": "how-browseract-fixed-the-stale-selector-failures-breaking-my-browser-tasks", "title": "How BrowserAct Fixed the Stale-Selector Failures Breaking My Browser Tasks", "summary": "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.", "body_md": "*Disclosure: BrowserAct sponsored this piece. The BrowserAct links below are affiliate-tracked — I get credit if you sign up through them.*\n\nI 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.\n\nI tested that against a page I built myself, compared directly against raw Playwright.\n\nHere's the shape of it. A frontend re-renders with fresh CSS-module or styled-components hashes on every deploy. You write:\n\n``` js\nconst id = await page.locator(\"button\").getAttribute(\"id\");\nawait page.reload();\nawait page.locator(`#${id}`).click({ timeout: 3000 });\n```\n\nThat 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:\n\n```\nlocator.click: Timeout 3000ms exceeded.\n```\n\nPlaywright's own role/text locators fix this specific case — `getByRole(\"button\", { name: \"Submit\" })`\n\nsurvives 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.\n\nBrowserAct 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.\n\n``` php\nstate  -> indexed list of interactive elements, as of right now\nclick <index>  -> act on one of them\nstate  -> read again, because the page may have changed\n```\n\nThe 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`\n\ncall would notice.\n\n```\nuv tool install browser-act-cli --python 3.12\nbrowser-act --version\nbrowser-act browser create --name \"dom-drift-test\" --type chrome --desc \"local churn test\"\nbrowser-act browser list\n```\n\n`browser list`\n\nafter 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`\n\nbrowser type (a local, blank browser, immediate to create) rather than `stealth`\n\n, since this test is about selector churn on a page I control, not anti-bot evasion — `stealth`\n\nrequires an API key and a billed purchase flow that has nothing to do with what I was testing. Creating a local `chrome`\n\nbrowser completed immediately with no purchase page involved.\n\nVersions tested: `browser-act-cli`\n\nv1.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.\n\nSkill source: [browser-act/skills](https://github.com/browser-act/skills/tree/main/browser-act). Installation details: [docs/installation.md](https://github.com/browser-act/skills/blob/main/docs/installation.md).\n\n```\nbrowser-act --session domtest browser open <browser-id> http://localhost:8934\nbrowser-act --session domtest state\n```\n\n`state`\n\nreturns an indexed list, not a selector:\n\n```\n[1]<button class=item-r8k7gx invalid=false />\n        Bravo\n[2]<button class=item-cr2ajb invalid=false />\n        Charlie\n[5]<button id=submit-btn-ckeewz invalid=false />\n        Submit\n```\n\nYou act on the index (`click 5`\n\n), 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.\n\nI 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:\n\n**Playwright, selector captured once, reused after reload:**\n\n```\nFAILED clicking #submit-btn-g9mpcm:\n  locator.click: Timeout 3000ms exceeded.\n```\n\n**BrowserAct, index captured once, reused after reload:**\n\n```\nError 210603: The snapshot belongs to a different page or tab than the\ncurrent one. Run 'browser-act state' again on the current page.\n```\n\nBoth refuse to silently click the wrong thing. But BrowserAct's error names the exact cause and the exact fix — re-run `state`\n\n— where Playwright's is a generic timeout you already have to know how to interpret.\n\nThat 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:\n\n```\nbrowser-act --session shot1 browser open <browser-id> http://localhost:8934\nbrowser-act --session shot1 state\n[5]<button id=submit-btn-5zhwmn invalid=false />\n        Submit\nload 1\n```\n\nPage changes, and the old reference is used anyway — the error from earlier, live:\n\n```\nbrowser-act --session shot1 reload\nbrowser-act --session shot1 click 5\nError 210603: The snapshot belongs to a different page or tab than the\ncurrent one. Run 'browser-act state' again on the current page.\n```\n\nRe-check, don't retry blind:\n\n```\nbrowser-act --session shot1 state\n[5]<button id=submit-btn-c66evj invalid=false />\n        Submit\nload 3\n```\n\nThe id changed (`submit-btn-5zhwmn`\n\nto `submit-btn-c66evj`\n\n), and the fresh `state`\n\ncall caught it. Acting on the new index closes the loop:\n\n```\nbrowser-act --session shot1 click 5\nclicked=5\n```\n\nState, act, hit the error, reassess, act again — completed, in the same session the error happened in.\n\nIt 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`\n\n, act on what's actually there now.\n\nTwo real limits, not softened. `state`\n\nonly 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`\n\nat all, it's BrowserAct's content-extraction commands (`get markdown`\n\n, `get text <index>`\n\n) — treating `state`\n\nas a universal page parser is the wrong mental model. And separately: the `title`\n\nfield in `state`\n\n's own output lagged behind the actual page content in one of my runs — the visible elements had already changed, `title`\n\nhadn't caught up yet. Worth knowing if you're tempted to key any check off `title`\n\nspecifically.\n\nBrowserAct 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`\n\nbrowser instance itself), the BrowserAct task session (`--session domtest`\n\n), and the target website's own authentication session.\n\nIn an earlier run against the same test page, the website authentication state expired while the BrowserAct task session remained active — `state`\n\nkept 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.\n\nClaude 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.\n\nTry BrowserAct: [browseract.ai/Daniel](https://www.browseract.ai/Daniel)\n\nBrowserAct Skills: [github.com/browser-act/skills](https://www.browseract.com/?co-from=Daniel&redirect=https://github.com/browser-act/skills/tree/main)\n\n*(Both links above are affiliate-tracked to my account.)*", "url": "https://wpnews.pro/news/how-browseract-fixed-the-stale-selector-failures-breaking-my-browser-tasks", "canonical_source": "https://dev.to/dannwaneri/how-browseract-fixed-the-stale-selector-failures-breaking-my-browser-tasks-52b5", "published_at": "2026-07-31 11:06:54+00:00", "updated_at": "2026-07-31 11:35:43.093690+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools"], "entities": ["BrowserAct", "Claude Code", "Playwright"], "alternates": {"html": "https://wpnews.pro/news/how-browseract-fixed-the-stale-selector-failures-breaking-my-browser-tasks", "markdown": "https://wpnews.pro/news/how-browseract-fixed-the-stale-selector-failures-breaking-my-browser-tasks.md", "text": "https://wpnews.pro/news/how-browseract-fixed-the-stale-selector-failures-breaking-my-browser-tasks.txt", "jsonld": "https://wpnews.pro/news/how-browseract-fixed-the-stale-selector-failures-breaking-my-browser-tasks.jsonld"}}