import { BROWSER_TOOLS } from '@ant/claude-for-chrome-mcp' import { chmod, mkdir, readFile, writeFile } from 'fs/promises' import { homedir } from 'os' import { join } from 'path' import { fileURLToPath } from 'url' import { getIsInteractive, getIsNonInteractiveSession, getSessionBypassPermissionsMode, } from '../../bootstrap/state.js' import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' import type { ScopedMcpServerConfig } from '../../services/mcp/types.js' import { isInBundledMode } from '../bundledMode.js' import { getGlobalConfig, saveGlobalConfig } from '../config.js' import { logForDebugging } from '../debug.js' import { getClaudeConfigHomeDir, isEnvDefinedFalsy, isEnvTruthy, } from '../envUtils.js' import { execFileNoThrowWithCwd } from '../execFileNoThrow.js' import { getPlatform } from '../platform.js' import { jsonStringify } from '../slowOperations.js' import { CLAUDE_IN_CHROME_MCP_SERVER_NAME, getAllBrowserDataPaths, getAllNativeMessagingHostsDirs, getAllWindowsRegistryKeys, openInChrome, } from './common.js' import { getChromeSystemPrompt } from './prompt.js' import { isChromeExtensionInstalledPortable } from './setupPortable.js' const CHROME_EXTENSION_RECONNECT_URL = 'https://clau.de/chrome/reconnect' const NATIVE_HOST_IDENTIFIER = 'com.anthropic.claude_code_browser_extension' const NATIVE_HOST_MANIFEST_NAME = `${NATIVE_HOST_IDENTIFIER}.json` export function shouldEnableClaudeInChrome(chromeFlag?: boolean): boolean { // Disable by default in non-interactive sessions (e.g., SDK, CI) if (getIsNonInteractiveSession() && chromeFlag !== true) { return false } // Check CLI flags if (chromeFlag === true) { return true } if (chromeFlag === false) { return false } // Check environment variables if (isEnvTruthy(process.env.CLAUDE_CODE_ENABLE_CFC)) { return true } if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_ENABLE_CFC)) { return false } // Check default config settings const config = getGlobalConfig() if (config.claudeInChromeDefaultEnabled !== undefined) { return config.claudeInChromeDefaultEnabled } return false } let shouldAutoEnable: boolean | undefined = undefined export function shouldAutoEnableClaudeInChrome(): boolean { if (shouldAutoEnable !== undefined) { return shouldAutoEnable } shouldAutoEnable = getIsInteractive() && isChromeExtensionInstalled_CACHED_MAY_BE_STALE() && (process.env.USER_TYPE === 'ant' || getFeatureValue_CACHED_MAY_BE_STALE('tengu_chrome_auto_enable', false)) return shouldAutoEnable } /** * Setup Claude in Chrome MCP server and tools * * @returns MCP config and allowed tools, or throws an error if platform is unsupported */ export function setupClaudeInChrome(): { mcpConfig: Record allowedTools: string[] systemPrompt: string } { const isNativeBuild = isInBundledMode() const allowedTools = BROWSER_TOOLS.map( tool => `mcp__claude-in-chrome__${tool.name}`, ) const env: Record = {} if (getSessionBypassPermissionsMode()) { env.CLAUDE_CHROME_PERMISSION_MODE = 'skip_all_permission_checks' } const hasEnv = Object.keys(env).length > 0 if (isNativeBuild) { // Create a wrapper script that calls the same binary with --chrome-native-host. This // is needed because the native host manifest "path" field cannot contain arguments. const execCommand = `"${process.execPath}" --chrome-native-host` // Run asynchronously without blocking; best-effort so swallow errors void createWrapperScript(execCommand) .then(manifestBinaryPath => installChromeNativeHostManifest(manifestBinaryPath), ) .catch(e => logForDebugging( `[Claude in Chrome] Failed to install native host: ${e}`, { level: 'error' }, ), ) return { mcpConfig: { [CLAUDE_IN_CHROME_MCP_SERVER_NAME]: { type: 'stdio' as const, command: process.execPath, args: ['--claude-in-chrome-mcp'], scope: 'dynamic' as const, ...(hasEnv && { env }), }, }, allowedTools, systemPrompt: getChromeSystemPrompt(), } } else { const __filename = fileURLToPath(import.meta.url) const __dirname = join(__filename, '..') const cliPath = join(__dirname, 'cli.js') void createWrapperScript( `"${process.execPath}" "${cliPath}" --chrome-native-host`, ) .then(manifestBinaryPath => installChromeNativeHostManifest(manifestBinaryPath), ) .catch(e => logForDebugging( `[Claude in Chrome] Failed to install native host: ${e}`, { level: 'error' }, ), ) const mcpConfig = { [CLAUDE_IN_CHROME_MCP_SERVER_NAME]: { type: 'stdio' as const, command: process.execPath, args: [`${cliPath}`, '--claude-in-chrome-mcp'], scope: 'dynamic' as const, ...(hasEnv && { env }), }, } return { mcpConfig, allowedTools, systemPrompt: getChromeSystemPrompt(), } } } /** * Get native messaging hosts directories for all supported browsers * Returns an array of directories where the native host manifest should be installed */ function getNativeMessagingHostsDirs(): string[] { const platform = getPlatform() if (platform === 'windows') { // Windows uses a single location with registry entries pointing to it const home = homedir() const appData = process.env.APPDATA || join(home, 'AppData', 'Local') return [join(appData, 'Claude Code', 'ChromeNativeHost')] } // macOS and Linux: return all browser native messaging directories return getAllNativeMessagingHostsDirs().map(({ path }) => path) } export async function installChromeNativeHostManifest( manifestBinaryPath: string, ): Promise { const manifestDirs = getNativeMessagingHostsDirs() if (manifestDirs.length === 0) { throw Error('Claude in Chrome Native Host not supported on this platform') } const manifest = { name: NATIVE_HOST_IDENTIFIER, description: 'Claude Code Browser Extension Native Host', path: manifestBinaryPath, type: 'stdio', allowed_origins: [ `chrome-extension://fcoeoabgfenejglbffodgkkbkcdhcgfn/`, // PROD_EXTENSION_ID ...(process.env.USER_TYPE === 'ant' ? [ 'chrome-extension://dihbgbndebgnbjfmelmegjepbnkhlgni/', // DEV_EXTENSION_ID 'chrome-extension://dngcpimnedloihjnnfngkgjoidhnaolf/', // ANT_EXTENSION_ID ] : []), ], } const manifestContent = jsonStringify(manifest, null, 2) let anyManifestUpdated = false // Install manifest to all browser directories for (const manifestDir of manifestDirs) { const manifestPath = join(manifestDir, NATIVE_HOST_MANIFEST_NAME) // Check if content matches to avoid unnecessary writes const existingContent = await readFile(manifestPath, 'utf-8').catch( () => null, ) if (existingContent === manifestContent) { continue } try { await mkdir(manifestDir, { recursive: true }) await writeFile(manifestPath, manifestContent) logForDebugging( `[Claude in Chrome] Installed native host manifest at: ${manifestPath}`, ) anyManifestUpdated = true } catch (error) { // Log but don't fail - the browser might not be installed logForDebugging( `[Claude in Chrome] Failed to install manifest at ${manifestPath}: ${error}`, ) } } // Windows requires registry entries pointing to the manifest for each browser if (getPlatform() === 'windows') { const manifestPath = join(manifestDirs[0]!, NATIVE_HOST_MANIFEST_NAME) registerWindowsNativeHosts(manifestPath) } // Restart the native host if we have rewritten any manifest if (anyManifestUpdated) { void isChromeExtensionInstalled().then(isInstalled => { if (isInstalled) { logForDebugging( `[Claude in Chrome] First-time install detected, opening reconnect page in browser`, ) void openInChrome(CHROME_EXTENSION_RECONNECT_URL) } else { logForDebugging( `[Claude in Chrome] First-time install detected, but extension not installed, skipping reconnect`, ) } }) } } /** * Register the native host in Windows registry for all supported browsers */ function registerWindowsNativeHosts(manifestPath: string): void { const registryKeys = getAllWindowsRegistryKeys() for (const { browser, key } of registryKeys) { const fullKey = `${key}\\${NATIVE_HOST_IDENTIFIER}` // Use reg.exe to add the registry entry // https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging void execFileNoThrowWithCwd('reg', [ 'add', fullKey, '/ve', // Set the default (unnamed) value '/t', 'REG_SZ', '/d', manifestPath, '/f', // Force overwrite without prompt ]).then(result => { if (result.code === 0) { logForDebugging( `[Claude in Chrome] Registered native host for ${browser} in Windows registry: ${fullKey}`, ) } else { logForDebugging( `[Claude in Chrome] Failed to register native host for ${browser} in Windows registry: ${result.stderr}`, ) } }) } } /** * Create a wrapper script in ~/.claude/chrome/ that invokes the given command. This is * necessary because Chrome's native host manifest "path" field cannot contain arguments. * * @param command - The full command to execute (e.g., "/path/to/claude --chrome-native-host") * @returns The path to the wrapper script */ async function createWrapperScript(command: string): Promise { const platform = getPlatform() const chromeDir = join(getClaudeConfigHomeDir(), 'chrome') const wrapperPath = platform === 'windows' ? join(chromeDir, 'chrome-native-host.bat') : join(chromeDir, 'chrome-native-host') const scriptContent = platform === 'windows' ? `@echo off REM Chrome native host wrapper script REM Generated by Claude Code - do not edit manually ${command} ` : `#!/bin/sh # Chrome native host wrapper script # Generated by Claude Code - do not edit manually exec ${command} ` // Check if content matches to avoid unnecessary writes const existingContent = await readFile(wrapperPath, 'utf-8').catch(() => null) if (existingContent === scriptContent) { return wrapperPath } await mkdir(chromeDir, { recursive: true }) await writeFile(wrapperPath, scriptContent) if (platform !== 'windows') { await chmod(wrapperPath, 0o755) } logForDebugging( `[Claude in Chrome] Created Chrome native host wrapper script: ${wrapperPath}`, ) return wrapperPath } /** * Get cached value of whether Chrome extension is installed. Returns * from disk cache immediately, updates cache in background. * * Use this for sync/startup-critical paths where blocking on filesystem * access is not acceptable. The value may be stale if the cache hasn't * been updated recently. * * Only positive detections are persisted. A negative result from the * filesystem scan is not cached, because it may come from a machine that * shares ~/.claude.json but has no local Chrome (e.g. a remote dev * environment using the bridge), and caching it would permanently poison * auto-enable for every session on every machine that reads that config. */ function isChromeExtensionInstalled_CACHED_MAY_BE_STALE(): boolean { // Update cache in background without blocking void isChromeExtensionInstalled().then(isInstalled => { // Only persist positive detections — see docstring. The cost of a stale // `true` is one silent MCP connection attempt per session; the cost of a // stale `false` is auto-enable never working again without manual repair. if (!isInstalled) { return } const config = getGlobalConfig() if (config.cachedChromeExtensionInstalled !== isInstalled) { saveGlobalConfig(prev => ({ ...prev, cachedChromeExtensionInstalled: isInstalled, })) } }) // Return cached value immediately from disk const cached = getGlobalConfig().cachedChromeExtensionInstalled return cached ?? false } /** * Detects if the Claude in Chrome extension is installed by checking the Extensions * directory across all supported Chromium-based browsers and their profiles. * * @returns Object with isInstalled boolean and the browser where the extension was found */ export async function isChromeExtensionInstalled(): Promise { const browserPaths = getAllBrowserDataPaths() if (browserPaths.length === 0) { logForDebugging( `[Claude in Chrome] Unsupported platform for extension detection: ${getPlatform()}`, ) return false } return isChromeExtensionInstalledPortable(browserPaths, logForDebugging) }