Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
14 kB · 427 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428/** * TMUX SOCKET ISOLATION * ===================== * This module manages an isolated tmux socket for Claude's operations. * * WHY THIS EXISTS: * Without isolation, Claude could accidentally affect the user's tmux sessions. * For example, running `tmux kill-session` via the Bash tool would kill the * user's current session if they started Claude from within tmux. * * HOW IT WORKS: * 1. Claude creates its own tmux socket: `claude-<PID>` (e.g., `claude-12345`) * 2. ALL Tmux tool commands use this socket via the `-L` flag * 3. ALL Bash tool commands inherit TMUX env var pointing to this socket * (set in Shell.ts via getClaudeTmuxEnv()) * * This means ANY tmux command run through Claude - whether via the Tmux tool * directly or via Bash - will operate on Claude's isolated socket, NOT the * user's tmux session. * * IMPORTANT: The user's original TMUX env var is NOT used. After socket * initialization, getClaudeTmuxEnv() returns a value that overrides the * user's TMUX in all child processes spawned by Shell.ts. */
import { posix } from 'path'import { registerCleanup } from './cleanupRegistry.js'import { logForDebugging } from './debug.js'import { toError } from './errors.js'import { execFileNoThrow } from './execFileNoThrow.js'import { logError } from './log.js'import { getPlatform } from './platform.js'
// Constants for tmux socket managementconst TMUX_COMMAND = 'tmux'const CLAUDE_SOCKET_PREFIX = 'claude'
/** * Executes a tmux command, routing through WSL on Windows. * On Windows, tmux only exists inside WSL — WSL interop lets the tmux session * launch .exe files as native Win32 processes while stdin/stdout flow through * the WSL pty. */async function execTmux( args: string[], opts?: { useCwd?: boolean },): Promise<{ stdout: string; stderr: string; code: number }> { if (getPlatform() === 'windows') { // -e execs tmux directly without the login shell. Without it, wsl hands the // command line to bash which eats `#` as a comment: `display-message -p // #{socket_path},#{pid}` below becomes `display-message -p ` → exit 1 → // we silently fall back to the guessed path and never learn the real // server PID. Same root cause as TungstenTool/utils.ts:execTmuxCommand. const result = await execFileNoThrow('wsl', ['-e', TMUX_COMMAND, ...args], { env: { ...process.env, WSL_UTF8: '1' }, ...opts, }) return { stdout: result.stdout || '', stderr: result.stderr || '', code: result.code || 0, } } const result = await execFileNoThrow(TMUX_COMMAND, args, opts) return { stdout: result.stdout || '', stderr: result.stderr || '', code: result.code || 0, }}
// Socket state - initialized lazily when Tmux tool is first used or a tmux command is runlet socketName: string | null = nulllet socketPath: string | null = nulllet serverPid: number | null = nulllet isInitializing = falselet initPromise: Promise<void> | null = null
// tmux availability - checked once upfrontlet tmuxAvailabilityChecked = falselet tmuxAvailable = false
// Track whether the Tmux tool has been used at least once// Used to defer socket initialization until actually neededlet tmuxToolUsed = false
/** * Gets the socket name for Claude's isolated tmux session. * Format: claude-<PID> */export function getClaudeSocketName(): string { if (!socketName) { socketName = `${CLAUDE_SOCKET_PREFIX}-${process.pid}` } return socketName}
/** * Gets the socket path if the socket has been initialized. * Returns null if not yet initialized. */export function getClaudeSocketPath(): string | null { return socketPath}
/** * Sets socket info after initialization. * Called after the tmux session is created. */export function setClaudeSocketInfo(path: string, pid: number): void { socketPath = path serverPid = pid}
/** * Returns whether the socket has been initialized. */export function isSocketInitialized(): boolean { return socketPath !== null && serverPid !== null}
/** * Gets the TMUX environment variable value for Claude's isolated socket. * * CRITICAL: This value is used by Shell.ts to override the TMUX env var * in ALL child processes. This ensures that any `tmux` command run via * the Bash tool will operate on Claude's socket, NOT the user's session. * * Format: "socket_path,server_pid,pane_index" (matches tmux's TMUX env var) * Example: "/tmp/tmux-501/claude-12345,54321,0" * * Returns null if socket is not yet initialized. * When null, Shell.ts does not override TMUX, preserving user's environment. */export function getClaudeTmuxEnv(): string | null { if (!socketPath || serverPid === null) { return null } return `${socketPath},${serverPid},0`}
/** * Checks if tmux is available on this system. * This is checked once and cached for the lifetime of the process. * * When tmux is not available: * - TungstenTool (Tmux) will not work * - TeammateTool will not work (it uses tmux for pane management) * - Bash commands will run without tmux isolation */export async function checkTmuxAvailable(): Promise<boolean> { if (!tmuxAvailabilityChecked) { const result = getPlatform() === 'windows' ? await execFileNoThrow('wsl', ['-e', TMUX_COMMAND, '-V'], { env: { ...process.env, WSL_UTF8: '1' }, useCwd: false, }) : await execFileNoThrow('which', [TMUX_COMMAND], { useCwd: false, }) tmuxAvailable = result.code === 0 if (!tmuxAvailable) { logForDebugging( `[Socket] tmux is not installed. The Tmux tool and Teammate tool will not be available.`, ) } tmuxAvailabilityChecked = true } return tmuxAvailable}
/** * Returns the cached tmux availability status. * Returns false if availability hasn't been checked yet. * Use checkTmuxAvailable() to perform the check. */export function isTmuxAvailable(): boolean { return tmuxAvailabilityChecked && tmuxAvailable}
/** * Marks that the Tmux tool has been used at least once. * Called by TungstenTool before initialization. * After this is called, Shell.ts will initialize the socket for subsequent Bash commands. */export function markTmuxToolUsed(): void { tmuxToolUsed = true}
/** * Returns whether the Tmux tool has been used at least once. * Used by Shell.ts to decide whether to initialize the socket. */export function hasTmuxToolBeenUsed(): boolean { return tmuxToolUsed}
/** * Ensures the socket is initialized with a tmux session. * Called by Shell.ts when the Tmux tool has been used or the command includes "tmux". * Safe to call multiple times; will only initialize once. * * If tmux is not installed, this function returns gracefully without * initializing the socket. getClaudeTmuxEnv() will return null, and * Bash commands will run without tmux isolation. */export async function ensureSocketInitialized(): Promise<void> { // Already initialized if (isSocketInitialized()) { return }
// Check if tmux is available before trying to use it const available = await checkTmuxAvailable() if (!available) { return }
// Another call is already initializing - wait for it but don't propagate errors // The original caller handles the error and sets up graceful degradation if (isInitializing && initPromise) { try { await initPromise } catch { // Ignore - the original caller logs the error } return }
isInitializing = true initPromise = doInitialize()
try { await initPromise } catch (error) { // Log error but don't throw - graceful degradation const err = toError(error) logError(err) logForDebugging( `[Socket] Failed to initialize tmux socket: ${err.message}. Tmux isolation will be disabled.`, ) } finally { isInitializing = false }}
/** * Kills the tmux server for Claude's isolated socket. * Called during graceful shutdown to clean up resources. */async function killTmuxServer(): Promise<void> { const socket = getClaudeSocketName() logForDebugging(`[Socket] Killing tmux server for socket: ${socket}`)
const result = await execTmux(['-L', socket, 'kill-server'])
if (result.code === 0) { logForDebugging(`[Socket] Successfully killed tmux server`) } else { // Server may already be dead, which is fine logForDebugging( `[Socket] Failed to kill tmux server (exit ${result.code}): ${result.stderr}`, ) }}
async function doInitialize(): Promise<void> { const socket = getClaudeSocketName()
// Create a new session with our custom socket // Pass CLAUDE_CODE_SKIP_PROMPT_HISTORY via -e so it's set in the initial shell environment // // On Windows, the tmux server inherits WSL_INTEROP from the short-lived // wsl.exe that spawns it; once `new-session -d` detaches and wsl.exe exits, // that socket stops servicing requests. Any cli.exe launched inside the pane // then hits `UtilAcceptVsock: accept4 failed 110` (ETIMEDOUT). Observed on // 2026-03-25: server PID 386 (started alongside /init at WSL boot) inherited // /run/WSL/383_interop — init's own socket, which listens but doesn't handle // interop. /run/WSL/1_interop is a stable symlink WSL maintains to the real // handler; pin the server to it so interop survives the spawning wsl.exe. const result = await execTmux([ '-L', socket, 'new-session', '-d', '-s', 'base', '-e', 'CLAUDE_CODE_SKIP_PROMPT_HISTORY=true', ...(getPlatform() === 'windows' ? ['-e', 'WSL_INTEROP=/run/WSL/1_interop'] : []), ])
if (result.code !== 0) { // Session might already exist from a previous run with same PID (unlikely but possible) // Check if the session exists const checkResult = await execTmux([ '-L', socket, 'has-session', '-t', 'base', ]) if (checkResult.code !== 0) { throw new Error( `Failed to create tmux session on socket ${socket}: ${result.stderr}`, ) } }
// Register cleanup to kill the tmux server on exit registerCleanup(killTmuxServer)
// Set CLAUDE_CODE_SKIP_PROMPT_HISTORY in the tmux GLOBAL environment (-g). // Without -g this would only apply to the 'base' session, and new sessions // created by TungstenTool (e.g. 'test', 'verify') would not inherit it. // Any Claude Code instance spawned on this socket will inherit this env var, // preventing test/verification sessions from polluting the user's real // command history and --resume session list. await execTmux([ '-L', socket, 'set-environment', '-g', 'CLAUDE_CODE_SKIP_PROMPT_HISTORY', 'true', ])
// Same WSL_INTEROP pin as the new-session -e above, but in the GLOBAL env // so sessions created by TungstenTool inherit it too. The -e on new-session // only covers the base session's initial shell; a later `new-session -s cc` // inherits the SERVER's env, which still holds the stale socket from the // wsl.exe that spawned it. if (getPlatform() === 'windows') { await execTmux([ '-L', socket, 'set-environment', '-g', 'WSL_INTEROP', '/run/WSL/1_interop', ]) }
// Get the socket path and server PID const infoResult = await execTmux([ '-L', socket, 'display-message', '-p', '#{socket_path},#{pid}', ])
if (infoResult.code === 0) { const [path, pidStr] = infoResult.stdout.trim().split(',') if (path && pidStr) { const pid = parseInt(pidStr, 10) if (!isNaN(pid)) { setClaudeSocketInfo(path, pid) return } } // Parsing failed - log and fall through to fallback logForDebugging( `[Socket] Failed to parse socket info from tmux output: "${infoResult.stdout.trim()}". Using fallback path.`, ) } else { // Command failed - log and fall through to fallback logForDebugging( `[Socket] Failed to get socket info via display-message (exit ${infoResult.code}): ${infoResult.stderr}. Using fallback path.`, ) }
// Fallback: construct the socket path from standard tmux location // tmux sockets are typically at $TMPDIR/tmux-<UID>/<socket_name> (or /tmp/tmux-<UID>/ if TMPDIR is not set) // On Windows this path is inside WSL, so always use POSIX separators. // process.getuid() is undefined on Windows; WSL default user is root (uid 0) in CI. const uid = process.getuid?.() ?? 0 const baseTmpDir = process.env.TMPDIR || '/tmp' const fallbackPath = posix.join(baseTmpDir, `tmux-${uid}`, socket)
// Get server PID separately const pidResult = await execTmux([ '-L', socket, 'display-message', '-p', '#{pid}', ])
if (pidResult.code === 0) { const pid = parseInt(pidResult.stdout.trim(), 10) if (!isNaN(pid)) { logForDebugging( `[Socket] Using fallback socket path: ${fallbackPath} (server PID: ${pid})`, ) setClaudeSocketInfo(fallbackPath, pid) return } // PID parsing failed logForDebugging( `[Socket] Failed to parse server PID from tmux output: "${pidResult.stdout.trim()}"`, ) } else { logForDebugging( `[Socket] Failed to get server PID (exit ${pidResult.code}): ${pidResult.stderr}`, ) }
throw new Error( `Failed to get socket info for ${socket}: primary="${infoResult.stderr}", fallback="${pidResult.stderr}"`, )}
// For testing purposesexport function resetSocketState(): void { socketName = null socketPath = null serverPid = null isInitializing = false initPromise = null tmuxAvailabilityChecked = false tmuxAvailable = false tmuxToolUsed = false}