Chrome for Claude (CFC) lets Claude Code read browser tabs, take screenshots,
and navigate pages via the --claude-in-chrome-mcp MCP server. Inside
devcontainers, the MCP server cannot reach the Chrome extension on the host.
dbr bridges this gap through Unix socket forwarding and a feature flag
override.
The Chrome extension communicates with Claude Code through a native messaging host binary. The lifecycle:
- Chrome launches
chrome-native-hostvia native messaging - The binary creates a Unix socket at
/tmp/claude-mcp-browser-bridge-<user>/<PID>.sock - Claude Code's MCP server connects to this socket
- Protocol: 4-byte little-endian length-prefixed JSON messages
- Socket is recreated each time Chrome restarts (new PID)
The MCP server has two ways to reach the Chrome extension:
| Mode | Trigger | Transport |
|---|---|---|
| Cloud bridge | tengu_copper_bridge: true |
WebSocket to wss://bridge.claudeusercontent.com |
| Local socket | tengu_copper_bridge: false |
Scans /tmp/claude-mcp-browser-bridge-<user>/ for .sock files |
Client selection logic (in CrR()):
- If bridge config exists and flag is true -> WebSocket bridge client
- Else -> scan
getSocketPaths()for local sockets, connect to first valid one
When connecting via local socket, the MCP server calls
validateSocketSecurity() which checks:
- Parent directory has mode
0700 - Socket file has mode
0600 - Socket owner UID matches the current user
It uses stat() (follows symlinks), not lstat(). This means dbr's mirror
sockets pass validation as long as they have correct permissions — which the
container daemon sets automatically.
Add the native host socket glob pattern to ~/.config/dbr/config.toml on the
host:
[socket_forwarding]
watch_paths = ["/tmp/claude-mcp-browser-bridge-*/*.sock"]When dbr discovers a matching socket on the host, it sends a SocketForward
message to the container daemon, which creates a mirror socket at the same path
inside the container. Connections to the mirror socket are bridged back to the
original host socket via dbr's reverse data channel.
The native host creates sockets under the host username:
/tmp/claude-mcp-browser-bridge-bradleybeddoes/12345.sock
Claude Code inside the container looks under the container username:
/tmp/claude-mcp-browser-bridge-vscode/
Since the MCP server's validateSocketSecurity() uses stat() (follows
symlinks), a symlink resolves this:
ln -sfn /tmp/claude-mcp-browser-bridge-bradleybeddoes \
/tmp/claude-mcp-browser-bridge-vscodeThe MCP server follows the symlink and finds the mirror socket.
Claude Code uses GrowthBook for feature flags, cached locally in
~/.claude.json under cachedGrowthBookFeatures. The flag
tengu_copper_bridge controls which connection mode the MCP server uses.
Inside containers, this flag defaults to true, causing the MCP server to
attempt the cloud WebSocket bridge. The Chrome extension on the host is not
connected to this bridge, so the MCP server reports "No Chrome extension
connected" even when dbr has successfully forwarded the native host socket.
The GrowthBook SDK refreshes cached flags in the background after Claude Code starts. This means:
- You edit
~/.claude.jsonto settengu_copper_bridge: false - Claude Code starts and reads the patched value (local socket mode)
- GrowthBook SDK refreshes in the background and overwrites the flag back
to
true - Next MCP server restart uses bridge mode again
The fix: patch the file immediately before launching Claude Code, every time. The MCP server reads the flag at startup, so as long as the patch is applied before the process starts, it works for that session.
A shell wrapper function patches the flag before each launch:
claude-chrome() {
python3 -c "
import json
f = '$HOME/.claude.json'
try:
d = json.load(open(f))
except (FileNotFoundError, json.JSONDecodeError):
d = {}
d.setdefault('cachedGrowthBookFeatures', {})['tengu_copper_bridge'] = False
json.dump(d, open(f, 'w'), indent=2)
" 2>/dev/null
command claude "$@"
}This sets tengu_copper_bridge to false before every launch, forcing the
local socket client which finds dbr's forwarded native host socket.
The MCP server is using bridge mode instead of local socket mode.
- Check the flag value:
python3 -c "import json; print(json.load(open('$HOME/.claude.json')).get('cachedGrowthBookFeatures', {}).get('tengu_copper_bridge'))" - If it prints
TrueorNone, the override is not being applied. Use theclaude-chromewrapper function. - If it prints
False, the MCP server may have already started with the old value. Restart Claude Code.
- Check
dbr status— the Socket Forwards section should list the native host socket:Socket Forwards Host Path Container Path /tmp/claude-mcp-browser-bridge-user/12345.sock /tmp/claude-mcp-browser-bridge-user/12345.sock - If no sockets are listed, verify
watch_pathsincludes the correct glob pattern in~/.config/dbr/config.toml. - Verify Chrome is running on the host with the Claude Code extension installed. The native host socket only exists when Chrome is running.
The MCP server's validateSocketSecurity() requires:
- Parent directory: mode
0700 - Socket file: mode
0600 - Owner UID matches current user
dbr creates mirror sockets with 0600 permissions. If the parent directory
has wrong permissions:
chmod 700 /tmp/claude-mcp-browser-bridge-$(whoami)When Chrome restarts, the native host binary gets a new PID, creating a new
socket (e.g., 67890.sock instead of 12345.sock). dbr detects this
automatically:
- The old socket disappears —
dbrsendsSocketUnforward - The new socket appears —
dbrsendsSocketForward - The container daemon creates a new mirror socket
No action needed. The MCP server reconnects automatically when it detects the socket change.
GrowthBook refreshes cached flags in the background. The claude-chrome
wrapper applies the override before each launch, which is sufficient because
the MCP server reads the flag at startup.
If the flag resets mid-session (e.g., after a long-running Claude Code
session), the MCP server may switch back to bridge mode on its next internal
restart. Exiting and relaunching with claude-chrome resolves this.
Claude Code caches whether the Chrome extension is installed in
~/.claude.json under cachedChromeExtensionInstalled. If this is false or
missing, Claude Code may not attempt to start the MCP server at all. Verify:
python3 -c "import json; print(json.load(open('$HOME/.claude.json')).get('cachedChromeExtensionInstalled'))"If false, install the Claude Code Chrome extension on the host and restart
Claude Code so it detects the extension.