diff --git a/packages/happy-cli/src/claude/sdk/utils.ts b/packages/happy-cli/src/claude/sdk/utils.ts index 0602d5a85..9b1cde7b5 100644 --- a/packages/happy-cli/src/claude/sdk/utils.ts +++ b/packages/happy-cli/src/claude/sdk/utils.ts @@ -77,6 +77,13 @@ export function getCleanEnv(): NodeJS.ProcessEnv { logger.debug('[Claude SDK] Removed Bun-specific environment variables for Node.js compatibility') } + // Remove Claude Code nested-session detection variables. + // When the daemon is started from within a Claude Code session, CLAUDECODE=1 leaks + // into all child processes. Claude Code detects this and refuses to start, causing + // every remote session to crash with "Process exited unexpectedly". + delete env.CLAUDECODE + delete env.CLAUDE_CODE_ENTRYPOINT + return env } diff --git a/packages/happy-cli/src/daemon/run.ts b/packages/happy-cli/src/daemon/run.ts index 75889d14e..473c8218a 100644 --- a/packages/happy-cli/src/daemon/run.ts +++ b/packages/happy-cli/src/daemon/run.ts @@ -408,6 +408,11 @@ export async function startDaemon(): Promise { // Add extra environment variables (these should already be filtered) Object.assign(tmuxEnv, extraEnv); + // Remove Claude Code nested-session detection variables so spawned + // sessions don't inherit them from a daemon started inside Claude Code. + delete tmuxEnv.CLAUDECODE; + delete tmuxEnv.CLAUDE_CODE_ENTRYPOINT; + const tmuxResult = await tmux.spawnInTmux([fullCommand], { sessionName: tmuxSessionName, windowName: windowName, @@ -497,14 +502,22 @@ export async function startDaemon(): Promise { // TODO: In future, sessionId could be used with --resume to continue existing sessions // For now, we ignore it - each spawn creates a new session + // Build a clean environment for the spawned session. + // Remove CLAUDECODE and CLAUDE_CODE_ENTRYPOINT to prevent Claude Code from + // detecting a "nested session" and refusing to start when the daemon was + // launched from within a Claude Code session. + const spawnEnv: Record = { + ...process.env, + ...extraEnv + }; + delete spawnEnv.CLAUDECODE; + delete spawnEnv.CLAUDE_CODE_ENTRYPOINT; + const happyProcess = spawnHappyCLI(args, { cwd: directory, detached: true, // Sessions stay alive when daemon stops stdio: ['ignore', 'pipe', 'pipe'], // Capture stdout/stderr for debugging - env: { - ...process.env, - ...extraEnv - } + env: spawnEnv }); // Log output for debugging diff --git a/packages/happy-cli/src/ui/logger.ts b/packages/happy-cli/src/ui/logger.ts index ecf739b61..63b788ab5 100644 --- a/packages/happy-cli/src/ui/logger.ts +++ b/packages/happy-cli/src/ui/logger.ts @@ -201,9 +201,11 @@ class Logger { } private logToFile(prefix: string, message: string, ...args: unknown[]): void { - const logLine = `${prefix} ${message} ${args.map(arg => - typeof arg === 'string' ? arg : JSON.stringify(arg) - ).join(' ')}\n` + const logLine = `${prefix} ${message} ${args.map(arg => { + if (typeof arg === 'string') return arg + if (arg instanceof Error) return arg.stack || arg.message + return JSON.stringify(arg) + }).join(' ')}\n` // Send to remote server if configured if (this.dangerouslyUnencryptedServerLoggingUrl) {