Sergen Tanguc
Publication: APR 2026 Reading time: 6 min

Making Playwright MCP Work With Your Real Browser in Claude Code

#claude-code #reverse-engineering

Making Playwright MCP Work With Your Real Browser in Claude Code

Environment

ComponentVersion
Claude Code2.1.89
@playwright/mcp0.0.70
playwright-core1.60.0-alpha
Playwright MCP Bridge extension0.0.66
Google Chrome146.0.7680.165
Node.js24.12.0
macOS15.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-data

No --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-data

Flag 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/bash
exec npx @playwright/mcp@latest --extension

Set 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 279
options.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 PLAYWRIGHT

The 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 file
result = mergeConfig(result, envOverrides); // env vars
result = 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-here

2. 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 present
if (process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN)
process.env.PLAYWRIGHT_MCP_EXTENSION = 'true';
const { program } = require('playwright-core/lib/utilsBundle');

Now the flow is:

  1. Claude Code starts playwright-mcp --user-data-dir ~/.playwright-data (hardcoded, can’t change)
  2. Patched binary sees PLAYWRIGHT_MCP_EXTENSION_TOKEN in env (from global settings)
  3. Sets PLAYWRIGHT_MCP_EXTENSION=true in env
  4. Config resolution reads the env var: config.extension = true
  5. if (config.extension) takes the WebSocket path, ignores --user-data-dir
  6. 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:

~/.claude/hooks/patch-playwright-mcp.sh
#!/bin/bash
MARKER="// 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 0
grep -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 encyclopedia

What I learned

  1. Read the source before guessing. I spent 6 attempts trying config-level fixes when the answer was in 3 lines of config.js.

  2. 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.

  3. 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.

  4. Durability matters. A fix that breaks on the next package update isn’t a fix. The SessionStart hook makes this survive indefinitely.

Setup checklist

  1. Install the Playwright MCP Bridge Chrome extension
  2. Copy the token from the extension’s status page
  3. Add to ~/.claude/settings.json under env:
    "PLAYWRIGHT_MCP_EXTENSION_TOKEN": "your-token-here"
  4. Create ~/.claude/hooks/patch-playwright-mcp.sh (see above), chmod +x it
  5. Add the SessionStart hook to settings.json
  6. Restart Claude Code
  7. 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.