| name | protocol-browser-anti-stall |
| description | Prevent browser automation from freezing, stalling, or colliding between parallel agents, and enforce manual, headed, real-user driving (never scripted). Standardizes on the playwright-cli (`npx --yes @playwright/cli@latest`) with named sessions (`-s=<name>`) so multiple agents each get their own isolated browser โ replacing the single-instance Playwright MCP, where one shared profile could only be locked by one process at a time. Covers session naming, headed mode, persistent auth profiles, the wait/anti-loop budget, evidence-before-retry, artifact paths, and cleanup. Use BEFORE any browser automation โ testing webapps, user-story walkthroughs, QA/UX audits, visual verification, or any task that drives a browser. |
| license | MIT |
Browser Anti-Stall Protocol (playwright-cli)
Apply these rules to EVERY browser action. No exceptions.
This repo drives browsers with playwright-cli, not the Playwright MCP. The MCP exposes one
browser per server and a persistent profile can only be locked by one process at a time, so
parallel agents on the same repo fight over tabs and profile locks. The CLI gives every agent its
own isolated browser via -s=<session>, costs far fewer tokens (no tool schemas or verbose trees
loaded into context), and runs natively in parallel shells.
Read references/mcp-to-cli-map.md if you encounter old browser_* MCP tool calls โ it maps
every tool to its CLI command. Read references/playwright-session-coordination.md before your
first command โ session naming, persistent logins (incl. the Google/CDP block), and cleanup.
Invocation โ always this form
PW="npx --yes @playwright/cli@latest"
$PW -s=<session> <command> [args]
-s=<session> is mandatory on every call. Name it after your task or branch
(-s=qa-checkout, -s=audit-ux-home). Two agents must never share a session name.
- Do not rely on a global
npm i -g install. Under fnm/nvm the global prefix is
per-shell and disappears; npx always resolves.
--json / --raw are available when you need machine-readable output.
0. Manual & headed โ never scripted (read first)
You are driving a real, visible browser to feel what a user feels. A green script proves
nothing about UX โ see the screen and watch the logs.
- Headed, always. The CLI defaults to headless โ you MUST pass
--headed on open.
If you cannot see the window, say so rather than proceeding blind.
- One real action at a time.
click, type, fill, select, hover, press, drag
exactly as a user would. Never chain a whole flow into one code snippet.
eval / run-code are inspection-only. Use them ONLY to read state (DOM, computed
styles, storage, perf) or to wait for an element โ never to click, type, navigate, or submit.
Driving the UI through code bypasses real events and hides the bug you are hunting.
- No test files, no runner. Do not write
*.spec.ts, run npx playwright test, or use
codegen. You are here to experience the flow, not automate past it.
- Look after every action. Fresh
snapshot + screenshot + console + requests, plus the
dev-server terminal. Real pain surfaces on screen and in logs, not in an assertion.
1. Session lifecycle
$PW -s=qa-checkout open --headed http://localhost:3000
$PW -s=qa-checkout goto http://localhost:3000/cart
$PW -s=qa-checkout snapshot
$PW -s=qa-checkout close
$PW list
$PW close-all
$PW kill-all
open starts a browser; goto navigates an already-open one. Calling open twice on the same
session is wasteful โ use goto.
- Close only your own session. Never
close-all while another agent may be mid-test.
- Add
--browser chrome|firefox|webkit|msedge, --device "iphone 15", or --mobile on open
when the task calls for it.
2. Navigation guard
After every open / goto / reload:
snapshot โ confirm the URL changed and the page has content.
- If blank or unchanged โ
sleep 2 โ snapshot again.
- Max 3 cycles (~6s). Still not loaded โ report a blocker (ยง8) and move on.
Never assume navigation succeeded without a snapshot to confirm it.
3. Waiting โ there is no wait command
Playwright auto-waits for actionability on click/fill/select, so most explicit waits are
unnecessary. When you genuinely must wait:
| Need | Do this |
|---|
| Fixed short pause | sleep 2 in the shell โ never more than 3s per pause |
| Wait for text/element | run-code "async (page) => { await page.getByText('Dashboard').first().waitFor({ timeout: 5000 }); return 'ready'; }" |
| Wait for something to disappear | ...waitFor({ state: 'hidden', timeout: 5000 }) |
| Poll for content | find "<text>" โ if no match, sleep 2 โ retry (max 3) |
Always set an explicit timeout (milliseconds) in waitFor โ the default 30s is far too long.
Use the incremental pattern instead of one long block:
sleep 2 โ snapshot โ check โ not ready
sleep 2 โ snapshot โ check โ not ready
sleep 2 โ snapshot โ check โ still not ready
STOP โ report blocker with evidence
This handles cold starts, SPA hydration, and slow APIs without ever blocking blindly.
4. SPA-specific rules
SPAs (React, Next.js, Vue) fire load before hydration completes โ never trust load events.
- Wait for a specific UI landmark that proves the app rendered (
run-code + waitFor, or find).
- If a spinner is showing, wait for it to reach
state: 'hidden' rather than sleeping.
5. Anti-loop: max 4 attempts per goal
| Attempt | Action |
|---|
| 1 | Try the action normally |
| 2 | Alternative approach โ re-snapshot for a fresh ref, try a CSS selector instead, scroll into view, or find the element |
| 3 | Gather evidence: console + requests |
| 4 | STOP. Report what blocked progress, with evidence. |
Never repeat the exact same failing action without new evidence.
Fresh refs after every state change. Refs from a stale snapshot are invalid after any
navigate/click/fill/hover/key press. Re-snapshot before the next interaction. click also accepts
a unique CSS selector, which survives state changes better than a ref.
6. Evidence before retry
When something is not working, gather evidence FIRST, then form a hypothesis:
console โ JS errors, warnings (console error to filter by level)
requests โ pending/failed calls; request <n> / response-body <n> for detail
snapshot โ the actual DOM state, not what you assume
screenshot --filename .playwright-mcp/<name>.png โ visual state
Only retry once you have a new hypothesis grounded in that evidence.
7. Timeout budget
| Scope | Max time |
|---|
| Single interaction (click, fill, select) | 15 seconds |
| Navigation + verification | 30 seconds |
| Multi-page flow | 5 minutes |
| Full session | 15 minutes |
Exceeded? Skip it and log [TIMEOUT] skipped: <step>. One stuck step must not kill the session.
8. Blocker reporting format
BLOCKER:
- Session: [-s= name]
- Page: [current URL]
- Goal: [what I was trying to do]
- Blocked by: [what prevented it]
- Evidence: [console errors / failed requests / screenshot observation]
- Suggestion: [most likely next step or manual action needed]
Actionable information beats a silent freeze.
9. Artifacts
- Screenshots, snapshots, and logs go under
.playwright-mcp/ (gitignored):
screenshot --filename .playwright-mcp/home-390.png. Name by route + viewport/step.
- The CLI also auto-writes snapshot
.yml files to .playwright-cli/ in the working directory โ
also gitignored, never committed.
- Sweep any stray root-level
*.png / *.log into .playwright-mcp/ before ending the session.
10. Parallel agents
Session isolation replaces the old tab-sharing etiquette โ each agent gets its own browser:
$PW -s=audit-ux open --headed โฆ $PW -s=qa-checkout open --headed โฆ
- Never reuse another agent's session name; never
close/kill-all sessions you did not open.
list shows every session with its status, profile, and headed flag โ check it before assuming.
- Within one session, multiple tabs are still available (
tab-list, tab-new, tab-select,
tab-close); the fresh-refs rule applies after every tab switch.
- Signed-in state is shared through persistent profiles, not shared tabs โ see
references/playwright-session-coordination.md.