-
-
Notifications
You must be signed in to change notification settings - Fork 397
Fix: Hide SDK subprocess console window on Windows #315
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix: Hide SDK subprocess console window on Windows #315
Conversation
On Windows, the Claude Agent SDK spawns a visible console window when calling query(). Use the spawnClaudeCodeProcess option to wrap spawn() with windowsHide: true, preventing the window from appearing. The spawnClaudeCodeProcess option is documented in the SDK types at: @anthropic-ai/claude-agent-sdk/entrypoints/agentSdkTypes.d.ts Fixes console window appearing on every user message for Windows users. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
|
I posted this to our discord:
I will merge this ASAP but after 5x trying to fix this issue and only being able to properly test on Mac / Linux... I'm going to wait until someone else can validate this fix as well. Seems like the actual real solution, since that windowsHide should have worked before, but you seem to have the detailed insights in to how it works from inside the api code. So I think this should work! And I'm sure it works on your machine. Just getting it double checked before merge. Thanks! :) |
|
I worked on my own fix for the blank consoles and resolved it for myself locally but decided to test this PR. Here is my result: PR #315 Windows Testing FeedbackSummaryTested PR #315 on Windows 11. The Root CauseThis is a known Node.js limitation documented in Node.js issue #21825:
The behavior persists in Bun as well, since Bun inherits Node.js process spawning semantics. Tested Code (PR #315)// ProcessManager.ts - Does NOT hide window on Windows
const child = spawn(bunPath, [script], {
detached: true,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, CLAUDE_MEM_WORKER_PORT: String(port) },
cwd: MARKETPLACE_ROOT,
...(isWindows && { windowsHide: true }) // <-- Ignored when detached: true
});Working SolutionUse PowerShell // ProcessManager.ts - Working fix
private static async startWithBun(script: string, logFile: string, port: number): Promise<{ success: boolean; pid?: number; error?: string }> {
const bunPath = getBunPath();
if (!bunPath) {
return {
success: false,
error: 'Bun is required but not found in PATH or common installation paths. Install from https://bun.sh'
};
}
try {
const isWindows = process.platform === 'win32';
if (isWindows) {
// Windows: Use PowerShell Start-Process with -WindowStyle Hidden
// This properly hides the console window (Node.js bug #21825 workaround)
const envVars = `$env:CLAUDE_MEM_WORKER_PORT='${port}'`;
const psCommand = `${envVars}; Start-Process -FilePath '${bunPath}' -ArgumentList '${script}' -WorkingDirectory '${MARKETPLACE_ROOT}' -WindowStyle Hidden -PassThru | Select-Object -ExpandProperty Id`;
const result = spawnSync('powershell', ['-Command', psCommand], {
stdio: 'pipe',
timeout: 10000,
windowsHide: true
});
if (result.status !== 0) {
return {
success: false,
error: `PowerShell spawn failed: ${result.stderr?.toString() || 'Unknown error'}`
};
}
const pid = parseInt(result.stdout.toString().trim(), 10);
if (isNaN(pid)) {
return { success: false, error: 'Failed to get PID from PowerShell' };
}
// Write PID file
this.writePidFile({
pid,
port,
startedAt: new Date().toISOString(),
version: process.env.npm_package_version || 'unknown'
});
return this.waitForHealth(pid, port);
}
// Unix: Standard spawn works fine
const child = spawn(bunPath, [script], {
detached: true,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, CLAUDE_MEM_WORKER_PORT: String(port) },
cwd: MARKETPLACE_ROOT
});
// Write logs
const logStream = createWriteStream(logFile, { flags: 'a' });
child.stdout?.pipe(logStream);
child.stderr?.pipe(logStream);
child.unref();
if (!child.pid) {
return { success: false, error: 'Failed to get PID from spawned process' };
}
// Write PID file
this.writePidFile({
pid: child.pid,
port,
startedAt: new Date().toISOString(),
version: process.env.npm_package_version || 'unknown'
});
return this.waitForHealth(child.pid, port);
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error)
};
}
}SDK Subprocess Window (SDKAgent.ts)The same issue applies to the SDK subprocess spawn. The spawnClaudeCodeProcess: (opts) => spawn(opts.command, opts.args, { ...opts, windowsHide: true })This may also need the PowerShell approach, or alternatively use Additional Finding: SDK Subprocess HangsDuring testing, I also discovered that the SDK subprocess can hang indefinitely. When this happens:
Fix: Added a watchdog timer that kills child processes before calling abort: function killChildProcesses(): void {
const isWindows = process.platform === 'win32';
try {
if (isWindows) {
execSync(`wmic process where "ParentProcessId=${process.pid}" delete`, {
stdio: 'ignore',
windowsHide: true,
timeout: 5000
});
} else {
execSync(`pkill -P ${process.pid}`, {
stdio: 'ignore',
timeout: 5000
});
}
} catch (error) {
// Ignore - child may already be dead
}
}
// In the watchdog timeout handler:
const resetWatchdog = () => {
if (watchdogTimer) clearTimeout(watchdogTimer);
watchdogTimer = setTimeout(() => {
logger.error('SDK', 'Query timeout - no response received, killing children and aborting');
killChildProcesses(); // Kill subprocess FIRST
session.abortController.abort(); // Then signal abort
}, SDK_QUERY_TIMEOUT_MS); // 2 minutes
};Bun Zombie Socket Issue on WindowsWhen the worker process terminates on Windows, Bun leaves TCP sockets in LISTEN state. The port remains bound even though no process owns it. This happens regardless of termination method (process.exit(), external kill, Ctrl+C). Symptoms:
Related Bun Issues:
Workarounds:
Recommendation for claude-mem: Consider switching the worker runtime from Bun to Node.js for Windows stability, or accept that users may need to reboot to clear zombie ports. Test Environment
Recommendation
|
Sounds good! Looking back at #304 it mentions multiple 'claude' windows spawning, whereas I was consistently just seeing one that persisted for the entirety of the time claude was processing and responding to a message. So there very well could be environment/config differences that make this not the complete fix, and more verification is absolutely welcome. Here's my environment details:
|
|
Now that I'm thinking about it. It's possible the PR actually solves the popup issue but the fix needs to also be applied to SDK subprocess spawn. The spawnClaudeCodeProcess. Since I'm testing with running subagents so maybe the worker now hides properly but the subagents don't yet. I'll test and report back |
|
Yeah so trying this PR's fixes with the subagent stuff didn't fix my popup issues. Both the worker and agent spawns generate the blank terminals. My powershell fixes seem to be needed for me. I am currently working on making a nice PR that will have the popups fixed, also the switch from bun to node for the worker to stop the zombie ports and also adding all kinds of recovery stuff to stuck messages in the queue and will add UI components to manage the queue to the web interface. Then I'll submit that. I've never submitted a PR to a public repo before so we'll see how that goes lol. I'll probably submit the PR tomorrow after I've thoroughly tested everything. |
|
@ToxMox Yeah I think you're right. I don't use any subagents, so I wasn't triggering worker startup on a regular basis. This PR only affects the windows coming from the Agent SDK calls. Edit: Finally got a chance to test and confirm that the powershell changes from above fix the issue I was targeting, without the need for changing the SDKAgent call. Closing this PR and would suggest going with ToxMox's more robust solution which hides the 'uvx' and 'node' windows, as well as at least 1 flashing cmd window that was coming from orphan cleanup attempts. |
WINDOWS FIXES (addresses issues from PR thedotmack#315): Zombie Socket Issue: - Switch worker from Bun to Node.js runtime - Bun left TCP sockets in LISTEN state after process termination on Windows - Users couldn't restart workers without rebooting the system - Node.js properly releases sockets on exit Blank Terminal Popups: - Use PowerShell Start-Process with -WindowStyle Hidden on Windows - Node.js bug #21825: windowsHide:true is ignored when detached:true - PowerShell workaround properly hides background worker windows SQLite Compatibility: - Add better-sqlite3 compatibility layer for Node.js - Replaces bun:sqlite which is Bun-specific - Same API surface, works on both runtimes QUEUE MONITORING UI: Background: The message queue could hang indefinitely when SDK subprocesses stalled during observation processing. AbortController.abort() didn't reliably terminate stuck subprocesses, causing queue deadlock. Solution: - Real-time queue drawer showing pending/processing/failed messages - Agent activity status indicator per session - Self-healing: auto-reset stuck processing messages when no active agent - Auto-restart SDK agent generator after self-healing - Batch message completion tracking (fixes SDK message batching) - Manual retry/abort controls and session recovery button - Watchdog service for crash recovery - Debug API endpoint (/api/debug/agent-log) for diagnostics - Persistent message queue with SQLite (survives crashes) Includes design docs in docs/plans/. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
WINDOWS FIXES (addresses issues from PR thedotmack#315): Zombie Socket Issue: - Switch worker from Bun to Node.js runtime - Bun left TCP sockets in LISTEN state after process termination on Windows - Users couldn't restart workers without rebooting the system - Node.js properly releases sockets on exit Blank Terminal Popups: - Use PowerShell Start-Process with -WindowStyle Hidden on Windows - Node.js bug #21825: windowsHide:true is ignored when detached:true - PowerShell workaround properly hides background worker windows SQLite Compatibility: - Add better-sqlite3 compatibility layer for Node.js - Replaces bun:sqlite which is Bun-specific - Same API surface, works on both runtimes QUEUE MONITORING UI: Background: The message queue could hang indefinitely when SDK subprocesses stalled during observation processing. AbortController.abort() didn't reliably terminate stuck subprocesses, causing queue deadlock. Solution: - Real-time queue drawer showing pending/processing/failed messages - Agent activity status indicator per session - Self-healing: auto-reset stuck processing messages when no active agent - Auto-restart SDK agent generator after self-healing - Batch message completion tracking (fixes SDK message batching) - Manual retry/abort controls and session recovery button - Watchdog service for crash recovery - Debug API endpoint (/api/debug/agent-log) for diagnostics - Persistent message queue with SQLite (survives crashes) Includes design docs in docs/plans/. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
WINDOWS FIXES (addresses issues from PR thedotmack#315): Zombie Socket Issue: - Switch worker from Bun to Node.js runtime - Bun left TCP sockets in LISTEN state after process termination on Windows - Users couldn't restart workers without rebooting the system - Node.js properly releases sockets on exit Blank Terminal Popups: - Use PowerShell Start-Process with -WindowStyle Hidden on Windows - Node.js bug #21825: windowsHide:true is ignored when detached:true - PowerShell workaround properly hides background worker windows SQLite Compatibility: - Add better-sqlite3 compatibility layer for Node.js - Replaces bun:sqlite which is Bun-specific - Same API surface, works on both runtimes QUEUE MONITORING UI: Background: The message queue could hang indefinitely when SDK subprocesses stalled during observation processing. AbortController.abort() didn't reliably terminate stuck subprocesses, causing queue deadlock. Solution: - Real-time queue drawer showing pending/processing/failed messages - Agent activity status indicator per session - Self-healing: auto-reset stuck processing messages when no active agent - Auto-restart SDK agent generator after self-healing - Batch message completion tracking (fixes SDK message batching) - Manual retry/abort controls and session recovery button - Watchdog service for crash recovery - Debug API endpoint (/api/debug/agent-log) for diagnostics - Persistent message queue with SQLite (survives crashes) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Summary
spawnClaudeCodeProcessoption to wrap spawn withwindowsHide: trueRelevant Issues
Fixes #304 (the visible 'claude' titled console window). PR #309 may prevent other utility windows from showing, but copying its fixes locally showed that they don't prevent the 'claude' one that persists for the duration of every response. Still doing investigation on the causes of the visible 'node' and 'uvx' windows.
Details
On Windows, the Claude Agent SDK spawns a visible console window when calling
query()for observation processing. This creates a poor UX for background services.The fix uses
spawnClaudeCodeProcess(found in SDK type definitions) to provide a custom spawn function that passeswindowsHide: trueto Node'sspawn(). This is a platform-safe option (no-op on non-Windows).API Stability Note
spawnClaudeCodeProcessis typed but not documented in the SDK's web documentation. We're relying on it as an escape hatch with the understanding that:Failure mode is benign - If Anthropic removes or changes this option:
Upstream issue filed - See anthropics/claude-agent-sdk-typescript#103 requesting they expose
windowsHideas a first-class option. If adopted, we could replacespawnClaudeCodeProcesswith a stable API (or remove the workaround entirely if they default totrue)Maintainer discretion - This fix improves Windows UX today at the cost of depending on an undocumented API. If the maintainer prefers to wait for an upstream fix, this PR can be closed.
Test plan
🤖 Generated with Claude Code