Making Playwright MCP Work With Your Real Browser in Claude Code
Making Playwright MCP Work With Your Real Browser in Claude Code
Environment
| Component | Version |
|---|---|
| Claude Code | 2.1.89 |
| @playwright/mcp | 0.0.70 |
| playwright-core | 1.60.0-alpha |
| Playwright MCP Bridge extension | 0.0.66 |
| Google Chrome | 146.0.7680.165 |
| Node.js | 24.12.0 |
| macOS | 15.7.3 (Sequoia) |
This was debugged on April 2, 2026. Claude Code’s Playwright plugin behavior may change in future versions — if they fix the hardcoded launch, this patch becomes unnecessary.
The problem
Playwright MCP has an extension mode that lets you connect to your actual running Chrome browser instead of spawning a disposable one. This is great — you get your logins, extensions, cookies, everything. The setup looks simple:
{ "mcpServers": { "playwright": { "command": "npx", "args": ["@playwright/mcp@latest", "--extension"], "env": { "PLAYWRIGHT_MCP_EXTENSION_TOKEN": "your-token-here" } } }}Install the Playwright MCP Bridge extension in Chrome, paste the token, and you’re done.
Except in Claude Code, this doesn’t work. At all.
Every time Claude Code starts the Playwright MCP server, it ignores your config and launches it with:
playwright-mcp --user-data-dir ~/.playwright-dataNo --extension flag. No env vars from your config. Just a hardcoded command that tries to open a separate Chrome instance — which fails because Chrome is already running with that user data directory.
The investigation
Attempt 1: just set the flag
Added --extension to the MCP config in settings.json. Restarted Claude Code.
ps aux | grep playwright-mcp# playwright-mcp --user-data-dir /Users/me/.playwright-dataFlag ignored. The process doesn’t have --extension.
Attempt 2: blame the plugin
Claude Code has a built-in Playwright plugin at ~/.claude/plugins/.../playwright/. Maybe it’s overriding my config.
Found the plugin’s .mcp.json:
{ "playwright": { "command": "npx", "args": ["@playwright/mcp@latest"] }}No --extension here. Edited it to add the flag. Restarted. The file was reset to the original on startup.
Deleted the plugin directory. It was recreated on startup.
Made it read-only. Claude Code still launched its hardcoded version.
Set "playwright@claude-plugins-official": false in enabledPlugins. Ignored.
Attempt 3: rename the server
Maybe Claude Code detects the name “playwright” and applies special handling. Renamed it to "browser" in settings.json.
Same result. Claude Code detects @playwright/mcp in the args, not the server name.
Attempt 4: wrapper script
Created a shell script that hides the package name:
#!/bin/bashexec npx @playwright/mcp@latest --extensionSet this as the MCP command. The plugin still took priority over settings.json, and Claude Code launched its own version.
Attempt 5: the CDP detour
Tried connecting directly to Chrome’s debug port:
"args": ["@playwright/mcp@latest", "--cdp-endpoint", "http://localhost:9222"]This requires launching Chrome with --remote-debugging-port=9222. But Chrome also requires a non-default user data directory for remote debugging (security measure). That means a separate profile — no logins, no extensions. Not what I wanted.
Attempt 6: environment variables
Finally read the actual source code of @playwright/mcp. In config.js:
// line 279options.extension = envToBoolean(e.PLAYWRIGHT_MCP_EXTENSION);Extension mode can be activated via the PLAYWRIGHT_MCP_EXTENSION env var. Put it in the plugin’s env config. Restarted.
Checked the process environment:
ps -E -p <pid> | tr ' ' '\n' | grep PLAYWRIGHTThe env var wasn’t there. Claude Code strips env vars from the plugin config too.
Reading the source
At this point I stopped guessing and read the full config resolution chain in playwright-core/lib/tools/mcp/config.js:
// resolveCLIConfigForMCP (called when launched as MCP server)let result = defaultConfig;result = mergeConfig(result, configInFile); // --config fileresult = mergeConfig(result, envOverrides); // env varsresult = mergeConfig(result, cliOverrides); // CLI args (last = highest priority)And the critical branching in program.js:
const config = await resolveCLIConfigForMCP(options);
if (config.extension) { // extension mode: connect to browser extension via WebSocket // does NOT launch Chrome, does NOT use userDataDir} else { // normal mode: launchPersistentContext with userDataDir}The key insight: env vars are read inside the process before CLI args are applied for config resolution. If PLAYWRIGHT_MCP_EXTENSION is true in the process environment, config.extension becomes true, and the extension code path is taken — regardless of what CLI args Claude Code injects.
But Claude Code strips env vars from the MCP config. How do you get an env var into the process without the config?
The solution
Two pieces:
1. Global env var
Claude Code has a global env section in settings.json that gets passed to all child processes, including MCP servers. This is meant for things like telemetry config, but it works for anything:
{ "env": { "PLAYWRIGHT_MCP_EXTENSION_TOKEN": "your-token-here" }}This token reaches the Playwright MCP process. Verified:
ps -E -p <pid> | tr ' ' '\n' | grep PLAYWRIGHT# PLAYWRIGHT_MCP_EXTENSION_TOKEN=your-token-here2. Binary patch
The token is in the environment, but PLAYWRIGHT_MCP_EXTENSION isn’t. We need the process itself to set it. The playwright-mcp binary lives in the npx cache at ~/.npm/_npx/.../node_modules/.bin/playwright-mcp. It’s a plain JavaScript file:
const { program } = require('playwright-core/lib/utilsBundle');const { decorateMCPCommand } = require('playwright-core/lib/tools/mcp/program');// ...Added two lines before the requires:
// force extension mode when token is presentif (process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN) process.env.PLAYWRIGHT_MCP_EXTENSION = 'true';
const { program } = require('playwright-core/lib/utilsBundle');Now the flow is:
- Claude Code starts
playwright-mcp --user-data-dir ~/.playwright-data(hardcoded, can’t change) - Patched binary sees
PLAYWRIGHT_MCP_EXTENSION_TOKENin env (from global settings) - Sets
PLAYWRIGHT_MCP_EXTENSION=truein env - Config resolution reads the env var:
config.extension = true if (config.extension)takes the WebSocket path, ignores--user-data-dir- Connects to the Chrome extension instead of launching a new browser
3. Auto-repair hook
The npx cache gets overwritten when @playwright/mcp updates. A SessionStart hook re-applies the patch:
#!/bin/bashMARKER="// force extension mode when PLAYWRIGHT_MCP_EXTENSION_TOKEN is set"BIN=$(find "$HOME/.npm/_npx" -name "playwright-mcp" \ -path "*/node_modules/.bin/*" 2>/dev/null | head -1)
[ -z "$BIN" ] && exit 0grep -q "$MARKER" "$BIN" 2>/dev/null && exit 0
sed -i.bak "s|const { program } = require('playwright-core/lib/utilsBundle');|$MARKER\nif (process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN)\n process.env.PLAYWRIGHT_MCP_EXTENSION = 'true';\n\nconst { program } = require('playwright-core/lib/utilsBundle');|" "$BIN"rm -f "${BIN}.bak"Register it in settings.json:
{ "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "bash \"~/.claude/hooks/patch-playwright-mcp.sh\"" } ] } ] }}The result
Claude Code now connects to my actual Chrome browser through the Playwright MCP Bridge extension. I can browse with my real session — logins, cookies, extensions, everything — and Claude interacts with it directly.
> Take a snapshot of the current page
### Page- Page URL: https://en.wikipedia.org/wiki/Main_Page- Page Title: Wikipedia, the free encyclopediaWhat I learned
-
Read the source before guessing. I spent 6 attempts trying config-level fixes when the answer was in 3 lines of
config.js. -
Env vars are the universal backdoor. Claude Code controls the CLI args but can’t control what happens inside the process. Global env vars + a 2-line patch was the entire fix.
-
Plugin systems can be hostile. Claude Code’s Playwright plugin ignores its own config file, ignores
enabledPlugins, auto-restores on deletion, and strips env vars from the MCP config. The only reliable escape was going through the global env. -
Durability matters. A fix that breaks on the next package update isn’t a fix. The SessionStart hook makes this survive indefinitely.
Setup checklist
- Install the Playwright MCP Bridge Chrome extension
- Copy the token from the extension’s status page
- Add to
~/.claude/settings.jsonunderenv:"PLAYWRIGHT_MCP_EXTENSION_TOKEN": "your-token-here" - Create
~/.claude/hooks/patch-playwright-mcp.sh(see above),chmod +xit - Add the SessionStart hook to
settings.json - Restart Claude Code
- Open Chrome, click the extension icon, select a tab to share
The token only needs updating if you regenerate it in the extension UI. One place to change: settings.json env section.