Playwright MCP: Fixing the Stuck Browser Reuse Problem
If you drive a browser through Playwright MCP from a tool like Claude Code, you may hit this: the browser from the previous task is still open, you start the next task, no new window appears, and nothing proceeds.
This article covers how I isolated the cause by reading the source of the installed @playwright/mcp (v0.0.78) directly, and how I fixed it with a SessionStart hook that safely cleans up only orphaned browser processes. It also answers, from the source rather than from guesswork, the question of whether one window can really only run one browser.
- What exactly is the browser reuse problem?
- Is “one window, one browser” a bug or the design?
- Can profile lock conflicts still happen with –isolated?
- So what was the actual cause?
- How does the SessionStart hook clean up orphans?
- What are the benefits and the limits of this approach?
- What alternatives were rejected?
- Why did the login state appear to persist?
- Practical tips
- Summary
What exactly is the browser reuse problem?
The symptom
The short version: the browser is supposed to be reused within a session, but the previous browser stays around in a non-functional state and the new task never starts.
Concretely, when you keep using Playwright MCP in the same VS Code window and the previously launched browser is still open, starting the next task produces no new browser window and execution stalls. Meanwhile, running in separate VS Code windows gives each one its own browser with no trouble at all. That “separate windows are fine” detail turned out to be the key clue.
What this article set out to do
Two goals, deliberately narrow:
- When the previous browser is broken or orphaned, clean it up automatically so the next run always gets a clean new browser
- Answer the question “can one window really only run one browser?” from the source code rather than by inference
Is “one window, one browser” a bug or the design?
It is intended behaviour, not a bug
The answer up front: one browser per VS Code window, meaning per session, is the intended design. It is not a defect.
Playwright MCP over a stdio connection starts one npx child process per VS Code window (session) and holds exactly one Browser instance inside it, reusing it across tasks. That is deliberate — it is how browser context such as your login state survives from one task to the next.
So running several windows at once is safe
Once you know the design, the reason separate windows work is obvious. A separate window means a separate process, which means a separate browser, so running Playwright MCP in several VS Code windows simultaneously causes no interference. The symptom is specific to reusing a browser inside one session; it is not a problem with parallel use.
Can profile lock conflicts still happen with –isolated?
No — a unique temporary profile is created on every launch
The profile lock conflicts that used to be common with Playwright tooling cannot occur in principle as long as --isolated is set.
When --isolated is present in the MCP configuration, Playwright creates a completely unique temporary profile with mkdtemp on each launch, and uses browserType.launch() rather than a persistent context. Because the profile is a different directory every time, the mcp-chrome-<hash> style lock conflict seen previously has nothing to conflict over. That is what the source code shows.
Why reading the primary source mattered
This investigation was a reminder of the value of going to the primary source. The first delegated investigation came back with the wrong answer — that lock conflicts happen even with --isolated — and reading the relevant playwright-core code directly is what proved it wrong and let me correct it. When a tool’s behaviour is unclear, reading the installed source is the shortest reliable path, not documentation and not second-hand accounts.
So what was the actual cause?
If lock conflicts are ruled out, two possibilities remain:
- A live reference being reused: within one session, trying to reuse a browser reference that is already open and stuck
- A leftover orphan process: a previous session terminated abnormally, leaving the Chromium process Playwright launched behind as an orphan
It was the second — an orphaned Chromium sitting there after its parent died — that actually blocked the next session from starting. Clean up exactly that and the next session always begins from a clean state.
How does the SessionStart hook clean up orphans?
What the hook consists of
The fix is lightweight: one hook added to the user-global Claude Code configuration.
- Add a
SessionStarthook to~/.claude/settings.json(user-global, so it applies to every project) - Create
~/.claude/hooks/cleanup-playwright-orphans.sh, which runs automatically at the start of each new session
Why it targets only orphans on Playwright-specific paths
The script’s matching is deliberately narrow. It only kills processes that are both orphaned (the parent has exited — on macOS orphans are reparented to PID 1) and have a Playwright MCP temporary profile path in their command line.
The two path markers it looks for:
playwright_chromiumdev_profile-(the temporary profile from--isolated)ms-playwright-mcp/mcp-(the older persistent profile)
Because a process has to satisfy both conditions — being orphaned and matching one of those paths — there is no risk of catching a browser that is alive in another window, or your everyday Chrome.
What are the benefits and the limits of this approach?
Benefits
- Safe: only orphans matching a Playwright-specific path are targeted. There is effectively no risk of killing a live browser or an ordinary Chrome
- Automatic: no more manual
pkill - No side effects on existing behaviour: the MCP server’s launch arguments and browser reuse logic are untouched, so workflows that depend on a persistent login are not broken
- Lightweight: a single instant
psandawkscan, once per session start
Limits
- It does not handle a live browser being left open. That is intended behaviour (reuse), so the hook deliberately leaves it alone
- The matching logic is macOS-specific (parent exits, process is reparented to PID 1). It may not hold on Linux or WSL, so it needs revisiting if the environment changes
- It only runs when a new session starts. An orphan created inside your current session survives until the next session begins
- It depends on pattern matching. If
@playwright/mcpchanges its temporary profile naming convention, the hook could quietly stop having any effect (though it would not misfire)
What alternatives were rejected?
Each of these was considered and dropped, on safety or operational cost.
| Option | Why it was rejected |
|---|---|
A broad kill such as pkill -f "playwright|chromium" |
Too likely to take out live browsers in other VS Code windows, or unrelated Chrome instances, when several windows are in use |
Pinning --user-data-dir to a random temporary path each time |
Cannot be combined with --isolated, which already achieves the same goal. Redundant |
Dropping --isolated and managing a persistent profile with an explicit state file |
Adds operational complexity without addressing the actual problem |
Why did the login state appear to persist?
It contradicts what –isolated is supposed to do
One important caveat. My own operating notes said that once you log in, the login state is retained in later sessions — but that contradicts the design of --isolated, which keeps the profile only in a temporary location and never persists it to disk.
Under --isolated, once the browser process fully exits, cookies and login information should go with it.
The browser process had simply never exited
The explanation is that the login state appearing to survive across sessions was almost certainly the same browser process staying alive for a long time in the same VS Code window, never having fully exited. Nothing was being persisted; it just had not been shut down.
That creates a trade-off:
- This hook only touches orphans, so it does not affect the login state of a live session
- But if you switch to calling
browser_closeafter every task in pursuit of a guaranteed clean window, you lose the login state each time and pay for it in repeated logins
“Always clean” and “never log in again” are difficult to have at the same time.
Practical tips
A few things worth knowing day to day:
- To check for and clear orphans right now:
~/.claude/hooks/cleanup-playwright-orphans.shcan be run on its own at any time. You do not have to wait for the next session - To guarantee a clean window inside the current session: call the
browser_closetool before the next task. That is the most reliable route, at the cost of the login state - This change goes in
~/.claude/settings.json(user-global), so it applies to Claude Code sessions in every project you work on - The hook takes effect from the next session you start. Windows that are already open will not pick it up until you close and reopen them
Summary
The Playwright MCP problem where the previous browser sticks around and blocks progress was not a profile lock conflict. The real cause was orphaned Chromium processes left behind by crashes and abnormal exits. And “one window, one browser” is intended behaviour that exists to preserve login state, so running several windows in parallel is safe.
The fix was a low-impact SessionStart hook that cleans up only processes that are both orphaned and matching a Playwright-specific path. If there is one lesson here, it is this: when a tool’s behaviour puzzles you, read the installed source rather than relying on second-hand accounts.
Note: this is based on investigation of @playwright/mcp at v0.0.78. Internal implementation and naming conventions may change in later versions.