Claude-in-Chrome MCP: extension executes but CLI receives 'not connected' error (Windows)

Status Closed — not planned
Reported on v2.1.31
Maintainer reply None cached
Activity 15 comments · opened Feb 4, 2026 · closed Apr 5, 2026

Bug Description

Claude-in-Chrome MCP extension is installed and enabled, but Claude Code CLI consistently receives "Browser extension is not connected" error, even though the extension actually executes the command on the Chrome side.

Steps to Reproduce

  1. Install Claude-in-Chrome extension in Chrome
  2. Verify extension status in Claude Code: Status: Enabled, Extension: Installed
  3. Run /chrome command — shows extension is configured
  4. Call tabs_context_mcp (or any other mcp__claude-in-chrome__* tool)
  5. Chrome side shows the tool executed successfully (e.g., "Tabs read" appears in the extension UI)
  6. CLI receives error: "Browser extension is not connected"

Expected Behavior

The tool result should be returned to the CLI after successful execution in Chrome.

Actual Behavior

The extension processes the request (visible in Chrome UI as "Tabs read"), but the CLI always receives the generic "Browser extension is not connected" error message instead of the actual result.

Environment

  • Claude Code version: 2.1.31
  • OS: Windows 10/11
  • Chrome: latest stable
  • Extension: Claude-in-Chrome (installed via claude.ai/chrome)

Troubleshooting Attempted

  • Reinstalled the extension
  • Restarted Chrome multiple times
  • Restarted Claude Code CLI multiple times
  • Verified login on claude.ai
  • Confirmed MCP status shows Enabled/Installed

Additional Context

The disconnect appears to be in the response path — the request reaches the extension and is executed, but the response does not make it back to the CLI. This suggests a possible WebSocket or message-passing issue on Windows.

View original on GitHub ↗

15 Comments

github-actions[bot] · 6 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/20862
  2. https://github.com/anthropics/claude-code/issues/21404
  3. https://github.com/anthropics/claude-code/issues/21211

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

vbp1 · 6 months ago

Root Cause Analysis

I make a little research and found that looks like the exact bug. The issue is a Windows-specific path mismatch between the native host and the MCP server's socket discovery logic.

How the architecture works

  1. Native host (claude.exe --chrome-native-host) — started by Chrome via native messaging, creates a Windows named pipe server at:

``
\.\pipe\claude-mcp-browser-bridge-{username}
`
This is handled by the
CN$() function which correctly checks os.platform() === "win32"`.

  1. MCP server (claude.exe --claude-in-chrome-mcp) — started by the CLI, needs to connect to the native host's pipe to forward tool calls.
  1. The MCP server config passes both:
  • socketPath: CN$() → correct: \.\pipe\claude-mcp-browser-bridge-{username}
  • getSocketPaths: yZIbroken on Windows
  1. The factory function GlI() checks: if getSocketPaths exists → create a pool client (WlI). The pool client only uses getSocketPaths() and ignores socketPath.

The bug

The yZI() function (getSocketPaths) does not handle Windows. It returns Unix-style paths:

function yZI() {
  let H = [];
  // Tries to scan /tmp/claude-mcp-browser-bridge-{user}/ for .sock files — fails on Windows
  let $ = evH(); // "/tmp/claude-mcp-browser-bridge-{username}"
  try { /* readdirSync($) — fails silently */ } catch {}
  
  // Returns paths like:
  //   C:\Users\{user}\AppData\Local\Temp\claude-mcp-browser-bridge-{user}
  //   /tmp/claude-mcp-browser-bridge-{user}
  let A = `claude-mcp-browser-bridge-${username}`;
  let L = path.join(os.tmpdir(), A);  // ← NOT a named pipe path
  let D = `/tmp/${A}`;                // ← NOT a named pipe path
  
  if (!H.includes(L)) H.push(L);
  if (L !== D && !H.includes(D)) H.push(D);
  return H;
}

Neither of these paths is the Windows named pipe \.\pipe\claude-mcp-browser-bridge-{username}. The pool client tries to connect to non-existent Unix domain sockets, times out after 5 seconds, and returns "Browser extension is not connected."

Meanwhile, CN$() correctly handles Windows:

function CN$() {
  if (os.platform() === "win32") return `\\.\pipe\${afE()}`;
  return path.join(evH(), `${process.pid}.sock`);
}

The fix

Add Windows handling to yZI():

function yZI() {
  if (os.platform() === "win32") {
    return [CN$()]; // Return the named pipe path
  }
  // ... existing Unix socket discovery code
}

Evidence from debug logs

MCP server "claude-in-chrome": Starting connection with timeout of 30000ms
MCP server "claude-in-chrome": Successfully connected to stdio server in 477ms  ← CLI↔MCP ok
MCP server "claude-in-chrome": Calling MCP tool: tabs_context_mcp
MCP server "claude-in-chrome": Tool 'tabs_context_mcp' completed successfully in 5s  ← 5s = timeout

The named pipe exists and accepts connections (verified manually via NamedPipeClientStream), but the MCP server never tries to connect to it because yZI() returns wrong paths.

This affects all Windows users of Claude-in-Chrome. The same bug likely exists in issues #20862, #21404, and #21211.

vbp1 · 6 months ago

Binary Patch for Windows (v2.1.31)

While waiting for an official fix, here is a same-length binary patch that fixes the yZI() function directly in claude.exe.

What it does

Adds if(Yy.platform()==="win32")return[CN$()]; to the getSocketPaths function so it returns the correct Windows named pipe path \.\pipe\claude-mcp-browser-bridge-{username} instead of non-existent Unix socket paths.

patch-claude-chrome.js

<details>
<summary>Click to expand patch script</summary>

#!/usr/bin/env node
/**
 * Binary patch for claude.exe to fix Claude-in-Chrome on Windows.
 *
 * Bug: yZI() (getSocketPaths) returns Unix-style paths on Windows instead of
 * the Windows named pipe path (\.\pipe\claude-mcp-browser-bridge-{user}).
 * The MCP server pool client uses yZI() to discover sockets, so it never
 * finds the native host's named pipe.
 *
 * Fix: Add an early return for win32 that calls CN$() (which correctly
 * returns the named pipe path). Remove the /tmp fallback to keep the
 * same byte length.
 *
 * Tested on: Claude Code v2.1.31
 * Usage: node patch-claude-chrome.js [path-to-claude.exe]
 */

const fs = require("fs");
const path = require("path");
const crypto = require("crypto");

const target = process.argv[2] || path.join(process.env.HOME || process.env.USERPROFILE, ".local", "bin", "claude.exe");

// Original function (281 bytes)
const ORIGINAL = Buffer.from(
  'function yZI(){let H=[],$=evH();try{let I=CZI.readdirSync($);for(let f of I)' +
  'if(f.endsWith(".sock"))H.push(wY.join($,f))}catch{}let A=`claude-mcp-browser-' +
  'bridge-${AzA()}`,L=wY.join(Yy.tmpdir(),A),D=`/tmp/${A}`;if(!H.includes(L))H.' +
  'push(L);if(L!==D&&!H.includes(D))H.push(D);return H}'
);

// Patched function (281 bytes) — same length
// Changes:
//   + Added: if(Yy.platform()==="win32")return[CN$()];  (early return with named pipe)
//   - Removed: ,D=`/tmp/${A}`;...if(L!==D&&!H.includes(D))H.push(D)  (/tmp fallback)
//   + Padding: /*W32*/ comment before closing brace
const PATCHED = Buffer.from(
  'function yZI(){if(Yy.platform()==="win32")return[CN$()];let H=[],$=evH();try{' +
  'let I=CZI.readdirSync($);for(let f of I)if(f.endsWith(".sock"))H.push(wY.join' +
  '($,f))}catch{}let A=`claude-mcp-browser-bridge-${AzA()}`,L=wY.join(Yy.tmpdir(' +
  '),A);if(!H.includes(L))H.push(L);return H /*W32*/}'
);

// Sanity checks
if (ORIGINAL.length !== PATCHED.length) {
  console.error(`FATAL: length mismatch — original=${ORIGINAL.length}, patched=${PATCHED.length}`);
  process.exit(1);
}
console.log(`Patch size: ${ORIGINAL.length} bytes (same-length replacement)`);

// Read binary
if (!fs.existsSync(target)) {
  console.error(`File not found: ${target}`);
  process.exit(1);
}

console.log(`Reading ${target}...`);
const bin = fs.readFileSync(target);
const sha256Before = crypto.createHash("sha256").update(bin).digest("hex");
console.log(`SHA-256 (before): ${sha256Before}`);
console.log(`Binary size: ${(bin.length / 1024 / 1024).toFixed(1)} MB`);

// Find all occurrences
const offsets = [];
let searchFrom = 0;
while (true) {
  const idx = bin.indexOf(ORIGINAL, searchFrom);
  if (idx === -1) break;
  offsets.push(idx);
  searchFrom = idx + 1;
}

if (offsets.length === 0) {
  // Check if already patched
  let patchedOffsets = [];
  searchFrom = 0;
  while (true) {
    const idx = bin.indexOf(PATCHED, searchFrom);
    if (idx === -1) break;
    patchedOffsets.push(idx);
    searchFrom = idx + 1;
  }
  if (patchedOffsets.length > 0) {
    console.log(`\nAlready patched! Found ${patchedOffsets.length} patched occurrence(s) at: ${patchedOffsets.map(o => '0x' + o.toString(16)).join(', ')}`);
    process.exit(0);
  }
  console.error("\nERROR: Original function not found in binary.");
  console.error("This binary may be a different version or already modified.");
  process.exit(1);
}

console.log(`\nFound ${offsets.length} occurrence(s) at offsets: ${offsets.map(o => '0x' + o.toString(16)).join(', ')}`);

// Patch
for (const offset of offsets) {
  PATCHED.copy(bin, offset);
  console.log(`  Patched at 0x${offset.toString(16)}`);
}

// Backup & write
const backupPath = target + ".bak";
if (!fs.existsSync(backupPath)) {
  console.log(`\nCreating backup: ${backupPath}`);
  fs.copyFileSync(target, backupPath);
} else {
  console.log(`\nBackup already exists: ${backupPath}`);
}

console.log(`Writing patched binary: ${target}`);
fs.writeFileSync(target, bin);

const sha256After = crypto.createHash("sha256").update(bin).digest("hex");
console.log(`SHA-256 (after):  ${sha256After}`);
console.log(`\nDone. ${offsets.length} occurrence(s) patched.`);
console.log("Original saved as .bak — restore with: copy claude.exe.bak claude.exe");

</details>

Usage

  1. Save the script as patch-claude-chrome.js
  2. Close all Claude Code sessions
  3. Run:
node patch-claude-chrome.js
  1. Start Claude Code with claude --chrome and verify

By default it patches %USERPROFILE%\.local\bin\claude.exe. To specify a different path:

node patch-claude-chrome.js "C:\path\to\claude.exe"

Rollback

cd %USERPROFILE%\.local\bin
del claude.exe
ren claude.exe.bak claude.exe

Notes

  • Tested on v2.1.31 only — other versions will have different minified symbol names and the patch won't apply (the script will report "Original function not found")
  • Creates a .bak backup before writing
  • Detects if already patched and skips
  • The patch will be overwritten by the next claude update
rvwcs-jimt · 6 months ago

For v2.1.34 I updated the patched constants as follows, and it now works:

// Original function (281 bytes)
const ORIGINAL = Buffer.from(
  'function iYI(){let H=[],$=IkH();try{let I=kYI.readdirSync($);for(let f of I)' +
  'if(f.endsWith(".sock"))H.push(yY.join($,f))}catch{}let A=`claude-mcp-browser-' +
  'bridge-${NzA()}`,L=yY.join(Ry.tmpdir(),A),D=`/tmp/${A}`;if(!H.includes(L))H.' +
  'push(L);if(L!==D&&!H.includes(D))H.push(D);return H}'
);

// Patched function (281 bytes) — same length
// Changes:
//   + Added: if(Ry.platform()==="win32")return[AZ$()];  (early return with named pipe)
//   - Removed: ,D=`/tmp/${A}`;...if(L!==D&&!H.includes(D))H.push(D)  (/tmp fallback)
//   + Padding: /*W32*/ comment before closing brace
const PATCHED = Buffer.from(
  'function iYI(){if(Ry.platform()==="win32")return[AZ$()];let H=[],$=IkH();try{' +
  'let I=kYI.readdirSync($);for(let f of I)if(f.endsWith(".sock"))H.push(yY.join' +
  '($,f))}catch{}let A=`claude-mcp-browser-bridge-${NzA()}`,L=yY.join(Ry.tmpdir(' +
  '),A);if(!H.includes(L))H.push(L);return H /*W32*/}'
);
bosmadev · 6 months ago

Root cause: \ missing Windows named pipe paths. The extension executes because the native host is running, but the MCP server can't connect back because it only looks for filesystem paths, not \ paths. Full analysis and fix: #23828

vbp1 · 6 months ago

Workaround: Using Chrome-in-Windows from Claude Code in WSL

For those running Claude Code inside WSL but using Chrome on the Windows host (like me, OMG), you can bridge the Windows named pipe to a WSL Unix socket using socat + npiperelay.exe.

Architecture

Claude Code (WSL) --stdio--> MCP server (WSL)
                                  |
                          connects to *.sock
                                  |
                        socat (UNIX-LISTEN)
                                  |
                          npiperelay.exe
                                  |
                    \\.\pipe\claude-mcp-browser-bridge-<WIN_USER>
                                  |
                    Chrome Native Messaging Host (Windows)
                                  |
                    Claude-in-Chrome extension (Windows Chrome)

Prerequisites

  • Chrome with Claude extension running on Windows
  • claude --chrome run at least once on Windows (to register the native host and create the pipe)
  • socat installed in WSL:

``bash
sudo apt install socat
``

  • npiperelay.exe built and placed in ~/utils/:

``bash
GOOS=windows GOARCH=amd64 go install github.com/jstarks/npiperelay@latest
cp ~/go/bin/windows_amd64/npiperelay.exe ~/utils/
``

1. Save the proxy script

cat > ~/utils/claude-chrome-wsl-proxy.sh << 'SCRIPT'
#!/bin/bash
# Bridge Windows named pipe → WSL Unix socket for Claude-in-Chrome.
# Uses npiperelay.exe + socat to properly bridge Windows named pipes
# from WSL (Node.js on Linux cannot access Windows pipes directly).
#
# Usage: bash claude-chrome-wsl-proxy.sh [WIN_USERNAME]

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NPIPERELAY="${SCRIPT_DIR}/npiperelay.exe"

if ! command -v socat &>/dev/null; then
  echo "Error: socat not found. Install with: sudo apt install socat" >&2
  exit 1
fi

if [[ ! -x "${NPIPERELAY}" ]]; then
  echo "Error: npiperelay.exe not found at ${NPIPERELAY}" >&2
  exit 1
fi

WIN_USER="${1:-$(cmd.exe /C "echo %USERNAME%" 2>/dev/null | tr -d '\r')}"
WSL_USER="$(whoami)"

PIPE="//./pipe/claude-mcp-browser-bridge-${WIN_USER}"
DIR="/tmp/claude-mcp-browser-bridge-${WSL_USER}"
SOCK="${DIR}/$$.sock"

mkdir -p "${DIR}" && chmod 700 "${DIR}"
rm -f "${SOCK}"

echo "Windows pipe: ${PIPE}"
echo "WSL socket:   ${SOCK}"
echo "npiperelay:   ${NPIPERELAY}"
echo "Press Ctrl+C to stop"

cleanup() {
  rm -f "${SOCK}"
  exit 0
}
trap cleanup INT TERM

socat UNIX-LISTEN:"${SOCK}",fork,mode=600 \
  EXEC:"${NPIPERELAY} -ep -s ${PIPE}",nofork
SCRIPT

chmod +x ~/utils/claude-chrome-wsl-proxy.sh

2. Startup order

a) Start Claude on Windows (PowerShell/cmd):

claude --chrome

Verify the named pipe exists:

[System.IO.Directory]::GetFiles("\\.\pipe\") | Where-Object { $_ -like "*claude*" }
# Expected: \\.\pipe\claude-mcp-browser-bridge-<WIN_USER>

b) Run the proxy in a separate WSL terminal:

# If Windows and WSL usernames match:
bash ~/utils/claude-chrome-wsl-proxy.sh

# If they differ, pass Windows username explicitly:
bash ~/utils/claude-chrome-wsl-proxy.sh MyWindowsUser

c) Run Claude Code in another WSL terminal:

claude --chrome

How it works

The proxy creates a Unix socket at /tmp/claude-mcp-browser-bridge-{wsl_user}/{pid}.sock — the path where the MCP server scans for .sock files on Linux. socat listens on this socket and for each connection runs npiperelay.exe to forward traffic to the Windows named pipe \\.\pipe\claude-mcp-browser-bridge-{win_user} where the native host is listening.

Verification

Check the proxy socket is listening:

ss -xlnp | grep claude-mcp-browser-bridge

Test the connection:

node -e "
const net = require('net');
const fs = require('fs');
const dir = '/tmp/claude-mcp-browser-bridge-' + require('os').userInfo().username;
const socks = fs.readdirSync(dir).filter(f => f.endsWith('.sock'));
console.log('Sockets found:', socks);
const c = net.createConnection(dir + '/' + socks[0]);
c.on('connect', () => { console.log('OK: connected'); c.destroy(); });
c.on('error', (e) => console.log('FAIL:', e.message));
c.on('close', () => process.exit(0));
setTimeout(() => process.exit(1), 3000);
"

Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| ECONNRESET on proxy test | Windows pipe has no listener | Start claude --chrome on Windows first |
| ENOENT on proxy test | Proxy socket missing | Restart the proxy script |
| "Browser extension is not connected" | Proxy not running or pipe mismatch | Check proxy is running; verify WIN_USERNAME matches |
| npiperelay.exe not found | Binary missing from ~/utils/ | Rebuild with GOOS=windows go install github.com/jstarks/npiperelay@latest |

bosmadev · 6 months ago

Comment for Related Issues (#23539, #23082, #21279, #20862)

---

Additional Root Cause: Windows Username Spaces

I've discovered a third independent bug affecting Windows users with spaces in their usernames (e.g., "John Smith", "Dennis Bosma").

The Problem

Pipe name mismatch:

Native host creates:  \\.\pipe\claude-mcp-browser-bridge-Dennis Bosma
MCP client expects:   \\.\pipe\claude-mcp-browser-bridge-DennisBosma
                                                          ^^^^ sanitized

This causes MCP tools to fail with "Browser extension is not connected" even when:

  • ✅ Extension shows "Enabled, Installed"
  • ✅ Chrome native messaging works (ping/status)
  • ❌ MCP tool calls timeout (pipe name mismatch)

Why This Matters

This bug persists EVEN AFTER applying existing workarounds:

  • ✅ Node.js instead of Bun → fixes stdin crash
  • ✅ Patched getSocketPaths() → fixes pipe discovery
  • Username space bug still blocks connection

This explains why some users report success with the Node.js + getSocketPaths() patch, but others still fail — it depends on whether they have spaces in their Windows username.

The Fix (One Line)

Add username sanitization in both native host and MCP client:

const username = os.userInfo().username.replace(/\s+/g, '');
const pipeName = `\\\\.\\pipe\\claude-mcp-browser-bridge-${username}`;

Test Evidence

Direct pipe connection test confirms the pipe with space exists:

const net = require('net');
const pipe = '\\\\.\\pipe\\claude-mcp-browser-bridge-Dennis Bosma';
net.createConnection(pipe).on('connect', () => console.log('✓ Pipe exists!'));
// Result: ✓ Pipe exists!

But MCP tools search for the sanitized version (without space) and fail.

Affected Users

  • ~30-50% of Windows users (default Windows setup creates usernames with spaces)
  • Corporate accounts: "FirstName LastName" format
  • Personal PCs: Full names entered during setup
  • Domain-joined: AD format "First Last"

Full Diagnosis

See Issue #23828 for comprehensive diagnosis, test evidence, and proposed fixes.

---

TL;DR: If you have spaces in your Windows username, the existing Bun + getSocketPaths() fixes won't help. You need username sanitization in the pipe name generation.

rvwcs-jimt · 6 months ago

Since we don't seem to be getting an upstream patch for this anytime soon, I've had claude generalise the original patch to survive basic recompiles. If the underlying code actually changes, this will break - but hopefully at that point this will be fixed.

#!/usr/bin/env node
/**
 * Binary patch for claude.exe to fix Claude-in-Chrome on Windows.
 *
 * Bug: getSocketPaths() returns Unix-style paths on Windows instead of
 * the Windows named pipe path (\\.\pipe\claude-mcp-browser-bridge-{user}).
 * The MCP server pool client uses getSocketPaths() to discover sockets,
 * so it never finds the native host's named pipe.
 *
 * Fix: Add an early return for win32 that calls the pipe path function
 * (which correctly returns the named pipe path). Remove the /tmp fallback
 * to keep the same byte length.
 *
 * Uses regex matching to tolerate minified identifier name changes between
 * builds. Only a structural rewrite of the function would break this.
 *
 * Usage: node patch-claude-chrome.js [path-to-claude.exe]
 */

const fs = require("fs");
const path = require("path");
const crypto = require("crypto");

const target =
  process.argv[2] ||
  path.join(
    process.env.HOME || process.env.USERPROFILE,
    ".local",
    "bin",
    "claude.exe"
  );

// ── Helpers ──────────────────────────────────────────────────────────

function escapeRegex(s) {
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

/**
 * Convert a code template with __name__ placeholders into a RegExp.
 * First occurrence of each placeholder → named capture group.
 * Subsequent occurrences → backreference \k<name>.
 */
function templateToRegex(template) {
  const ID = "[a-zA-Z_$][\\w$]*";
  const tokenRe = /__(\w+)__/g;
  const seen = new Set();
  const parts = [];
  let lastIndex = 0;
  let m;

  while ((m = tokenRe.exec(template)) !== null) {
    parts.push(escapeRegex(template.slice(lastIndex, m.index)));
    const name = m[1];
    if (!seen.has(name)) {
      parts.push(`(?<${name}>${ID})`);
      seen.add(name);
    } else {
      parts.push(`\\k<${name}>`);
    }
    lastIndex = tokenRe.lastIndex;
  }
  parts.push(escapeRegex(template.slice(lastIndex)));
  return new RegExp(parts.join(""), "g");
}

/** Replace __name__ placeholders with captured values. */
function fillTemplate(template, captures) {
  return template.replace(/__(\w+)__/g, (_, name) => {
    if (!(name in captures)) throw new Error(`Missing capture: ${name}`);
    return captures[name];
  });
}

// ── Function structure templates ─────────────────────────────────────
//
// __name__ tokens stand in for minified identifiers.
// All other text is structural and must match literally.

const ORIGINAL_TMPL =
  "function __fn__(){" +
  "let __arr__=[],__dir__=__sockDirFn__();" +
  "try{let __entries__=__fsMod__.readdirSync(__dir__);" +
  "for(let __entry__ of __entries__)" +
  'if(__entry__.endsWith(".sock"))__arr__.push(__pathMod__.join(__dir__,__entry__))' +
  "}catch{}" +
  "let __bridge__=`claude-mcp-browser-bridge-${__userFn__()}`," +
  "__tmpPath__=__pathMod__.join(__osMod__.tmpdir(),__bridge__)," +
  "__fallback__=`/tmp/${__bridge__}`;" +
  "if(!__arr__.includes(__tmpPath__))__arr__.push(__tmpPath__);" +
  "if(__tmpPath__!==__fallback__&&!__arr__.includes(__fallback__))__arr__.push(__fallback__);" +
  "return __arr__}";

// Patched: adds early win32 return, removes /tmp fallback.
// No closing } — padding is added dynamically to match original byte length.
const PATCHED_TMPL =
  "function __fn__(){" +
  'if(__osMod__.platform()==="win32")return[__pipeFn__()];' +
  "let __arr__=[],__dir__=__sockDirFn__();" +
  "try{let __entries__=__fsMod__.readdirSync(__dir__);" +
  "for(let __entry__ of __entries__)" +
  'if(__entry__.endsWith(".sock"))__arr__.push(__pathMod__.join(__dir__,__entry__))' +
  "}catch{}" +
  "let __bridge__=`claude-mcp-browser-bridge-${__userFn__()}`," +
  "__tmpPath__=__pathMod__.join(__osMod__.tmpdir(),__bridge__);" +
  "if(!__arr__.includes(__tmpPath__))__arr__.push(__tmpPath__);" +
  "return __arr__";

// ── Pipe function discovery ──────────────────────────────────────────

/**
 * Find the function that returns the socket/pipe path for the current platform.
 * Uses the already-captured osMod identifier to search for a parameterless
 * function whose body starts with: if(<osMod>.platform()==="win32")return
 */
function findPipeFn(source, osMod) {
  const ID = "[a-zA-Z_$][\\w$]*";
  const re = new RegExp(
    "function (" + ID + ")\\(\\)\\{" +
    "if\\(" + escapeRegex(osMod) + '\\.platform\\(\\)==="win32"\\)return'
  );
  const m = source.match(re);
  if (!m) {
    throw new Error(
      "Could not find pipe function (looked for " + osMod + ".platform===\"win32\" pattern)."
    );
  }
  return m[1];
}

// ── Main ─────────────────────────────────────────────────────────────

if (!fs.existsSync(target)) {
  console.error(`File not found: ${target}`);
  process.exit(1);
}

console.log(`Reading ${target} ...`);
const bin = fs.readFileSync(target);
const sha256Before = crypto.createHash("sha256").update(bin).digest("hex");
console.log(`SHA-256 (before): ${sha256Before}`);
console.log(`Binary size:      ${(bin.length / 1024 / 1024).toFixed(1)} MB`);

// latin1 preserves 1:1 byte<->char mapping so string offsets = byte offsets
const source = bin.toString("latin1");

// Build search regex from template
const searchRe = templateToRegex(ORIGINAL_TMPL);

// Find all matches
const matches = [];
let m;
while ((m = searchRe.exec(source)) !== null) {
  matches.push({
    index: m.index,
    length: m[0].length,
    groups: { ...m.groups },
  });
}

if (matches.length === 0) {
  // Check if already patched — look for our distinctive win32 early-return
  const alreadyPatched =
    /if\([a-zA-Z_$][\w$]*\.platform\(\)==="win32"\)return\[[a-zA-Z_$][\w$]*\(\)\];let [a-zA-Z_$][\w$]*=\[\]/.test(
      source
    );
  if (alreadyPatched) {
    console.log("\nAlready patched (found win32 early-return).");
    process.exit(0);
  }
  console.error("\nERROR: Original function pattern not found.");
  console.error("The function structure may have changed in this version.");
  process.exit(1);
}

console.log(`\nFound ${matches.length} occurrence(s).`);
const caps = matches[0].groups;
console.log("Identifiers:");
for (const [k, v] of Object.entries(caps)) console.log(`  ${k} = ${v}`);

// Find the pipe path function (uses osMod captured from main match)
const pipeFnName = findPipeFn(source, caps.osMod);
console.log(`  pipeFn = ${pipeFnName}`);
const allCaps = { ...caps, pipeFn: pipeFnName };

// Build patched code (without closing brace — padding added per-match)
const patchedCore = fillTemplate(PATCHED_TMPL, allCaps);

// Apply each match
for (const match of matches) {
  const need = match.length; // must produce exactly this many bytes
  const bare = patchedCore.length + 1; // +1 for closing }

  if (bare > need) {
    console.error(
      `\nPatch (${bare}b) exceeds original (${need}b) — cannot fit.`
    );
    process.exit(1);
  }

  const pad = need - bare;
  let patched;
  if (pad === 0) {
    patched = patchedCore + "}";
  } else if (pad < 4) {
    patched = patchedCore + " ".repeat(pad) + "}";
  } else {
    // Use a comment for readable padding
    patched = patchedCore + "/*" + " ".repeat(pad - 4) + "*/}";
  }

  if (patched.length !== need) {
    console.error(`FATAL: length mismatch (${patched.length} vs ${need})`);
    process.exit(1);
  }

  Buffer.from(patched, "latin1").copy(bin, match.index);
  console.log(`Patched at 0x${match.index.toString(16)} (${need} bytes)`);
}

// Backup & write
const bak = target + ".bak";
if (!fs.existsSync(bak)) {
  console.log(`\nBackup: ${bak}`);
  fs.copyFileSync(target, bak);
} else {
  console.log(`\nBackup exists: ${bak}`);
}

console.log(`Writing: ${target}`);
fs.writeFileSync(target, bin);

const sha256After = crypto.createHash("sha256").update(bin).digest("hex");
console.log(`SHA-256 (after):  ${sha256After}`);
console.log(
  `\nDone — ${matches.length} occurrence(s) patched.\n` +
    "Restore with: copy claude.exe.bak claude.exe"
);
bosmadev · 6 months ago

Update: ESM Bug & Two Patch Approaches (v2.1.37)

New finding: ESM module incompatibility

cli.js in v2.1.37 uses "type": "module" (ESM). Any patch using require("os") or require("path") will throw ReferenceError: require is not defined at runtime. Use process.platform and process.env.USERNAME instead — these are process globals available in both CJS and ESM.

Two complementary approaches

1. Binary patch for claude.exe (by @rvwcs-jimt above)

  • Patches the Bun standalone binary directly
  • Works for users who launch via claude.exe
  • Uses regex template matching for minified name resilience
  • Adds if(osMod.platform()==="win32")return[pipeFn()] early return

2. cli.js text patch via npm wrapper (our approach in #23828)

  • Patches cli.js on disk (both npm global and isolated Chrome install)
  • Requires launching via claude.cmd (npm global wrapper) instead of claude.exe
  • Uses content-based function discovery (anchor on claude-mcp-browser-bridge, brace counting)
  • Self-contained ESM-safe injection: if(process.platform==="win32"){let W=\\\.\pipe\claude-mcp-browser-bridge-${process.env.USERNAME||"default"}\;if(!A.includes(W))A.push(W)}
  • Auto-heals on updates via SessionStart hook

Both confirmed working end-to-end on Windows v2.1.37.

rvwcs-jimt · 6 months ago

I think this is fixed now? The patch no-longer applies and I can connect claude to chrome without it.

radaco99 · 6 months ago

What is your version?

I think this is fixed now? The patch no-longer applies and I can connect claude to chrome without it.
bosmadev · 6 months ago
I think this is fixed now? The patch no-longer applies and I can connect claude to chrome without it.

@rvwcs-jimt What version are you on? As of v2.1.39, the upstream code still doesn't include Windows named pipe paths in getSocketPaths(). If the old patch no longer applies, it's likely because the minified function name changed between versions (it goes from Gc4 to cc4 to $c4 etc. on each release).

The fix I maintain uses content-based pattern matching (anchors on the claude-mcp-browser-bridge string, not function names) so it survives across versions.

Published fix: bosmadev/claude — scripts/fix-chrome-native-host.py

Runs as a SessionStart hook, auto-repairs on every launch. Fixes all 3 Windows Chrome bugs (Bun crash, socket path discovery, bridge exclusive mode). See #23828 for details.

radaco99 · 5 months ago

when this bug is going to get fixed?

github-actions[bot] · 4 months ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.

github-actions[bot] · 4 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.