Claude Code is a terminal application, but it has a surprisingly deep surface area of desktop integration: it can hand off a running session to Claude Desktop, auto-detect the IDE it was launched from (VS Code, Cursor, JetBrains family), wire a Chrome extension through a native messaging host, and expose a full computer-use MCP server. All of these are implemented as discrete subsystems that share no global state — each activates only when its conditions are met.
commands/desktop/index.ts ·
commands/desktop/desktop.tsx ·
components/DesktopHandoff.tsx ·
components/DesktopUpsell/DesktopUpsellStartup.tsx ·
utils/desktopDeepLink.ts ·
utils/claudeDesktop.ts ·
utils/ide.ts ·
utils/claudeInChrome/ ·
utils/computerUse/ ·
entrypoints/cli.tsx
There are four distinct integration layers, each with its own detection logic, activation path, and fallback behavior:
Claude Desktop Handoff
The /desktop command flushes the session and opens it in the desktop app via a deep link URL.
IDE Integration
Auto-detects VS Code, Cursor, Windsurf, and 14+ JetBrains IDEs by reading lockfiles in ~/.claude/ide/.
Claude in Chrome
Installs a native messaging host manifest so a browser extension can call Claude Code tools directly.
Computer Use MCP
Gated by Max/Pro subscription and a GrowthBook flag; spawns a separate MCP server that drives the OS.
/desktop Command and Deep LinksCommand registration
The /desktop command (aliased /app) lives in
commands/desktop/index.ts. Its availability is locked to the
claude-ai product — it does not appear in SDK or enterprise console builds.
The command also gate-checks the platform at runtime:
// commands/desktop/index.ts
function isSupportedPlatform(): boolean {
if (process.platform === 'darwin') return true
if (process.platform === 'win32' && process.arch === 'x64') return true
return false
}
const desktop = {
type: 'local-jsx',
name: 'desktop',
aliases: ['app'],
description: 'Continue the current session in Claude Desktop',
availability: ['claude-ai'],
isEnabled: isSupportedPlatform,
get isHidden() { return !isSupportedPlatform() },
load: () => import('./desktop.js'),
}
When enabled, the command renders the DesktopHandoff React/Ink component. On
Windows only x64 is supported; ARM Windows and Linux are excluded.
The handoff state machine
DesktopHandoff in components/DesktopHandoff.tsx is a small
state machine with six states:
The minimum required desktop version is hard-coded as MIN_DESKTOP_VERSION = '1.1.2396'
in utils/desktopDeepLink.ts. Older installs are caught and the user is prompted to
update before the handoff proceeds.
Deep link construction
The actual URL that kicks off the handoff is built by buildDesktopDeepLink:
// utils/desktopDeepLink.ts
function buildDesktopDeepLink(sessionId: string): string {
// dev builds use 'claude-dev://', prod uses 'claude://'
const protocol = isDevMode() ? 'claude-dev' : 'claude'
const url = new URL(`${protocol}://resume`)
url.searchParams.set('session', sessionId)
url.searchParams.set('cwd', getCwd())
return url.toString()
}
The URL format is claude://resume?session=<uuid>&cwd=<path>. Claude
Desktop registers the claude:// protocol handler via Electron's
setAsDefaultProtocolClient. In development, AppleScript is used instead of
open because the dev Electron binary registers itself rather than the app bundle:
// macOS dev mode workaround
const { code } = await execFileNoThrow('osascript', [
'-e',
`tell application "Electron" to open location "${deepLinkUrl}"`,
])
Platform-specific install detection
Before opening the deep link, the code verifies the app is actually installed using OS-appropriate methods:
Path check
Checks /Applications/Claude.app exists. Version read via defaults read …/Info.plist CFBundleShortVersionString.
xdg-mime query
Runs xdg-mime query default x-scheme-handler/claude and checks stdout is non-empty (exit code alone is unreliable).
Registry query
Queries HKEY_CLASSES_ROOT\claude via reg query. Version discovered in %LOCALAPPDATA%\AnthropicClaude\app-*\.
Session flush: the secret ingredient
Before opening the deep link, the flushing state calls
flushSessionStorage(). This forces all buffered conversation turns to be written
to disk in ~/.claude/projects/ so that Claude Desktop can read the full
session transcript when it opens the claude://resume URL. Without the flush,
the desktop app would open an incomplete session.
Separate from the explicit /desktop command, Claude Code can proactively suggest
migrating to the desktop app at startup. The logic lives in
components/DesktopUpsell/DesktopUpsellStartup.tsx.
When the dialog appears
The function shouldShowDesktopUpsellStartup() gates the dialog:
export function shouldShowDesktopUpsellStartup(): boolean {
if (!isSupportedPlatform()) return false
// Requires GrowthBook flag 'tengu_desktop_upsell'.enable_startup_dialog
if (!getDesktopUpsellConfig().enable_startup_dialog) return false
const config = getGlobalConfig()
if (config.desktopUpsellDismissed) return false // "Never" was chosen
if ((config.desktopUpsellSeenCount ?? 0) >= 3) return false // capped at 3 showings
return true
}
The dialog offers three options: Open in Claude Code Desktop (triggers the
full handoff flow), Not now (increments the seen counter), and
Never ask again (sets desktopUpsellDismissed: true in
~/.claude/config.json via saveGlobalConfig). The feature is
controlled by the tengu_desktop_upsell GrowthBook experiment, which ships
disabled by default.
desktopUpsellDismissed and desktopUpsellSeenCount are stored
in the global config at ~/.claude/config.json. Deleting that file resets
the suppression state.
Claude Code can read the MCP server configuration from Claude Desktop and import it. The utility
is in utils/claudeDesktop.ts, which knows the config file locations on each platform:
// utils/claudeDesktop.ts
export async function getClaudeDesktopConfigPath(): Promise<string> {
if (platform === 'macos') {
return join(homedir(), 'Library', 'Application Support', 'Claude',
'claude_desktop_config.json')
}
// WSL: tries USERPROFILE, then scans /mnt/c/Users/* for AppData/Roaming/Claude/
}
The file is parsed to extract the mcpServers map. Each entry is validated against
McpStdioServerConfigSchema() (Zod) before being accepted. The
MCPServerDesktopImportDialog component renders a multi-select list so users can
cherry-pick which servers to copy into their Claude Code config.
C:\Users\moiz\AppData\Roaming\Claude are
converted to /mnt/c/Users/moiz/AppData/Roaming/Claude by stripping the drive
letter and prepending /mnt/c.
When Claude Code runs inside a terminal embedded in an IDE, it detects the IDE and
adjusts behavior — for example, using the IDE's file-open API instead of the system editor,
or routing "side queries" (quick questions) through a faster channel. Detection happens in
utils/ide.ts.
Supported IDEs
The supportedIdeConfigs map in ide.ts covers two families,
identified by the ideKind field:
cursor, windsurf, vscode
Detected by process names like Cursor Helper, Code Helper. Transport: HTTP/WebSocket to a local port.
intellij, pycharm, webstorm, phpstorm, rubymine, clion, goland, rider, datagrip, appcode, dataspell, aqua, gateway, fleet, androidstudio
15 IDEs total. Transport: SSE or WebSocket via a lockfile-advertised port.
Lockfile-based discovery
IDE plugins write a lockfile to ~/.claude/ide/<port>.lock when they start.
Claude Code reads these to find which IDEs are running and which workspace folders they have open:
// utils/ide.ts — lockfile JSON schema
type LockfileJsonContent = {
workspaceFolders?: string[]
pid?: number
ideName?: string
transport?: 'ws' | 'sse'
runningInWindows?: boolean
authToken?: string
}
Lockfiles are sorted by modification time (newest first) so the most recently focused IDE
is preferred. Stale locks are skipped — the code calls isProcessRunning(pid)
via process.kill(pid, 0) to verify the PID is still alive before trusting
the lockfile.
Terminal detection shortcut
Before scanning lockfiles, the code checks the TERM_PROGRAM environment variable
(stored in env.terminal). If it matches a known IDE process name (e.g.,
vscode, cursor), isSupportedTerminal() returns
true immediately without any filesystem I/O.
Windows-in-WSL path conversion
When the IDE is running in native Windows but Claude Code is running inside WSL, file paths
need conversion. utils/idePathConversion.ts provides
WindowsToWSLConverter which transforms
C:\Users\moiz\project into /mnt/c/Users/moiz/project, and
checkWSLDistroMatch validates that both processes are targeting the same
WSL distribution before accepting a cross-boundary connection.
Claude Code can act as a native messaging host for a Chrome (or Chromium) extension.
This allows the browser extension to invoke Claude Code tools from a webpage. The
subsystem lives under utils/claudeInChrome/.
Entrypoint flags
Two special CLI flags are handled at the very top of entrypoints/cli.tsx,
before any normal startup:
// entrypoints/cli.tsx
if (process.argv[2] === '--claude-in-chrome-mcp') {
// Runs the MCP server that the Chrome extension connects to
await runClaudeInChromeMcpServer()
return
} else if (process.argv[2] === '--chrome-native-host') {
// Acts as the Chrome native messaging host (stdin/stdout protocol)
await runChromeNativeHost()
return
}
Native messaging host manifest
Chrome requires a JSON manifest file that maps a host identifier to the binary path.
Claude Code auto-installs this manifest at startup when shouldEnableClaudeInChrome()
returns true:
const NATIVE_HOST_IDENTIFIER = 'com.anthropic.claude_code_browser_extension'
// In bundled (native binary) mode, a wrapper script is created that calls:
// `"${process.execPath}" --chrome-native-host`
// because native host manifests cannot contain CLI arguments directly.
const execCommand = `"${process.execPath}" --chrome-native-host`
void createWrapperScript(execCommand)
.then(manifestBinaryPath => installChromeNativeHostManifest(manifestBinaryPath))
Supported browsers
The CHROMIUM_BROWSERS map in utils/claudeInChrome/common.ts covers
multiple Chromium-based browsers, each with platform-specific data paths and native messaging
host directories:
export const CHROMIUM_BROWSERS: Record<ChromiumBrowser, BrowserConfig> = {
chrome: { macos: { nativeMessagingPath: ['Library', 'Application Support',
'Google', 'Chrome', 'NativeMessagingHosts'] }, ... },
// Also: chromium, brave, edge, opera, vivaldi, arc ...
}
Activation conditions
shouldEnableClaudeInChrome() checks in priority order:
- Disabled by default in non-interactive (SDK/CI) sessions
--chrome/--no-chromeCLI flags override everythingCLAUDE_CODE_ENABLE_CFCenvironment variableclaudeInChromeDefaultEnabledfield in~/.claude/config.json- Auto-enable: interactive session + extension already installed +
tengu_chrome_auto_enableGrowthBook flag
The most powerful (and most gated) desktop integration is computer use — the ability to take screenshots, move the mouse, type keystrokes, and interact with arbitrary desktop apps. It is exposed as a separate MCP server under the codename "Chicago" / "Malort".
Entrypoint flag
// entrypoints/cli.tsx
if (feature('CHICAGO_MCP') && process.argv[2] === '--computer-use-mcp') {
await runComputerUseMcpServer()
return
}
The feature('CHICAGO_MCP') call is a build-time dead-code-elimination gate:
the entire branch is removed from external builds that don't enable the flag.
Access gates
utils/computerUse/gates.ts implements a layered gate check:
export function getChicagoEnabled(): boolean {
// Ant employees with monorepo access are excluded unless ALLOW_ANT_COMPUTER_USE_MCP=1
if (process.env.USER_TYPE === 'ant' && process.env.MONOREPO_ROOT_DIR
&& !isEnvTruthy(process.env.ALLOW_ANT_COMPUTER_USE_MCP)) {
return false
}
// External users need Max or Pro subscription + GrowthBook gate
return hasRequiredSubscription() && readConfig().enabled
}
The GrowthBook experiment key is tengu_malort_pedway. It ships a
ChicagoConfig object that includes fine-grained sub-gates:
type ChicagoConfig = CuSubGates & {
enabled: boolean
coordinateMode: 'pixels' | 'normalized'
pixelValidation: boolean
clipboardPasteMultiline: boolean
mouseAnimation: boolean
hideBeforeAction: boolean // hide terminal before screenshot
autoTargetDisplay: boolean
clipboardGuard: boolean
}
Terminal bundle ID awareness
Because Claude Code is a terminal app with no window, the computer-use package needs to know
which terminal emulator is hosting it so it can exclude that window from screenshots and avoid
accidentally blocking its own keyboard input. utils/computerUse/common.ts resolves
this via __CFBundleIdentifier (set by macOS LaunchServices) with a static fallback
table for well-known terminals:
const TERMINAL_BUNDLE_ID_FALLBACK = {
'iTerm.app': 'com.googlecode.iterm2',
'Apple_Terminal': 'com.apple.Terminal',
'ghostty': 'com.mitchellh.ghostty',
'WarpTerminal': 'dev.warp.Warp-Stable',
'vscode': 'com.microsoft.VSCode',
// ...
}
The coordinate mode (pixels vs normalized) is frozen at first
read — a live GrowthBook flip mid-session would otherwise cause the model to think in one
coordinate space while the executor transforms in another.
All desktop-related subsystems hook into the CLI via special argument checks at the very top
of entrypoints/cli.tsx, before any module loading or config initialization.
This keeps the code paths for native host modes lean:
feature CHICAGO_MCP required] B -->|--daemon-worker| F[runDaemonWorker] B -->|remote-control / rc| G[bridgeMain] B -->|normal| H[full CLI boot] C --> Z[exit] D --> Z E --> Z F --> Z G --> Z
Each fast path returns immediately after its handler completes, so there is zero overhead from the normal boot pipeline in host/server modes. This matters for Chrome native messaging — Chrome spawns the host binary on demand and expects it to respond within milliseconds.
Claude Code ships two distribution modes: an npm-installed JavaScript bundle and a
pre-compiled native binary. The native installer in utils/nativeInstaller/
manages the lifecycle of the native binary alongside the npm installation.
Directory layout
# XDG-compliant layout
~/.local/share/claude/versions/ # permanent — one dir per installed version
~/.cache/claude/staging/ # temporary — download target before atomic rename
~/.local/state/claude/locks/ # PID-based lock files for update coordination
~/.local/bin/claude # symlink → active version
The installer keeps VERSION_RETENTION_COUNT = 2 versions on disk, deleting older
ones after a successful update. Updates are multi-process safe via PID-based lockfiles with a
7-day stale timeout (long enough to survive laptop sleep).
Platform targeting
getPlatform() in the installer module produces strings like
darwin-arm64, linux-x64, linux-x64-musl,
win32-x64. The musl variant is auto-detected via
envDynamic.isMuslEnvironment() for Alpine Linux / Docker environments.
utils/sessionStoragePortable.ts contains pure-Node.js utilities deliberately free
of internal dependencies. This file is shared verbatim with the VS Code extension package at
packages/claude-vscode/src/common-host/sessionStorage.ts — the same code reads
session transcripts in both the CLI and the IDE plugin.
(terminal)"] subgraph Desktop["Claude Desktop (Electron)"] DP["claude://resume
deep link handler"] DS["Session reader
(~/.claude/projects/)"] end subgraph IDE["IDE Plugin (VS Code / JetBrains)"] LF["~/.claude/ide/*.lock"] RP["Local HTTP/WS
RPC server"] end subgraph Browser["Chrome Extension"] NM["Native Messaging
com.anthropic.claude_code_browser_extension"] MCP["MCP Server
(--claude-in-chrome-mcp)"] end subgraph OS["OS (macOS)"] CU["Computer Use MCP
(--computer-use-mcp)
screenshots, mouse, keyboard"] end CLI -->|"/desktop command\nflushSession + open URL"| DP DP --> DS CLI -->|"reads lockfiles\nauto-detects"| LF LF --> RP CLI -->|"installs manifest\nspawns on demand"| NM NM --> MCP CLI -->|"Max/Pro + GB flag"| CU
Key Takeaways
- The
/desktopcommand is a 6-state React machine that flushes the session transcript, builds aclaude://resume?session=…&cwd=…URL, then hands off to the desktop app via OS-native URL opening. - Desktop install detection is OS-specific: path check on macOS,
xdg-mimeon Linux, registry query on Windows. - IDE detection uses
~/.claude/ide/<port>.lockfiles written by the VS Code and JetBrains plugins, supplemented by theTERM_PROGRAMenvironment variable. - Claude in Chrome works via Chrome's native messaging protocol — a JSON manifest maps the identifier
com.anthropic.claude_code_browser_extensionto a wrapper script that invokes the CLI with--chrome-native-host. - Computer use is triple-gated: build-time feature flag (
CHICAGO_MCP), Max/Pro subscription check, and GrowthBook experiment keytengu_malort_pedway. - All desktop-mode fast paths (
--chrome-native-host,--claude-in-chrome-mcp,--computer-use-mcp) are handled before any config or analytics initialization to keep startup overhead minimal. utils/sessionStoragePortable.tsis intentionally dependency-free so it can be shared with the VS Code extension package without pulling in CLI-only modules.
Knowledge Check
/desktop handoff to proceed?DesktopHandoff component call flushSessionStorage() before opening the deep link?openDeepLink use AppleScript instead of the open command?