Skip to content

fix: stop button not working on Windows for Claude Code agents#556

Open
jSydorowicz21 wants to merge 2 commits intoRunMaestro:mainfrom
jSydorowicz21:fix/stop-claude
Open

fix: stop button not working on Windows for Claude Code agents#556
jSydorowicz21 wants to merge 2 commits intoRunMaestro:mainfrom
jSydorowicz21:fix/stop-claude

Conversation

@jSydorowicz21
Copy link

@jSydorowicz21 jSydorowicz21 commented Mar 11, 2026

On Windows, child.kill('SIGINT') is unreliable for shell-spawned processes (This may be honored for users who specify a different terminal). The signal is silently ignored, leaving the agent running when users click stop.

Changes:

  • interrupt(): Write Ctrl+C (\x03) to stdin on Windows instead of SIGINT, with 2-second escalation to kill() if the process doesn't exit
  • kill(): Use taskkill /pid /t /f on Windows to terminate the entire process tree instead of SIGTERM, which doesn't reliably kill shell-spawned children
  • Use execFile with args array (async, no shell) to avoid command injection and main thread blocking
  • Add logging for Windows interrupt paths for debuggability

Unix/macOS behavior is unchanged.

Summary by CodeRabbit

  • Bug Fixes
    • Improved process termination on Windows: attempts a graceful interrupt via console input when available before escalating to forced termination.
    • Better handling for child processes across platforms with clearer escalation flow and fallback to forceful kill when interrupts fail.
    • Enhanced logging and messaging for interrupt vs. kill actions to make shutdown behavior more transparent.

On Windows, child.kill('SIGINT') is unreliable for shell-spawned processes
(which all agents are on Windows due to PATH resolution requiring shell: true).
The signal is silently ignored, leaving the agent running when users click stop.

Changes:
- interrupt(): Write Ctrl+C (\x03) to stdin on Windows instead of SIGINT,
  with 2-second escalation to kill() if the process doesn't exit
- kill(): Use taskkill /pid /t /f on Windows to terminate the entire process
  tree instead of SIGTERM, which doesn't reliably kill shell-spawned children
- Use execFile with args array (async, no shell) to avoid command injection
  and main thread blocking
- Add logging for Windows interrupt paths for debuggability

Unix/macOS behavior is unchanged.
@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 217e291b-98ab-47f4-bdeb-7f6af6ffc144

📥 Commits

Reviewing files that changed from the base of the PR and between 9567c7f and 99e02f8.

📒 Files selected for processing (1)
  • src/main/process-manager/ProcessManager.ts

📝 Walkthrough

Walkthrough

ProcessManager adds Windows-specific termination: attempts Ctrl+C on shell children via stdin, falls back to taskkill to kill process trees. Non-Windows retains signal-based SIGINT/SIGTERM flow; escalation messaging changed to reference "interrupt" before kill.

Changes

Cohort / File(s) Summary
ProcessManager Windows Process Termination
src/main/process-manager/ProcessManager.ts
Imported execFile and isWindows. Added private killWindowsProcessTree(pid, sessionId) using taskkill. Interrupt logic: on Windows attempt Ctrl+C to child stdin (log if unavailable), else use signal on non-Windows. Escalation message changed from "SIGINT" wording to "interrupt". Kill logic: Windows path uses killWindowsProcessTree when PID present, otherwise falls back to SIGTERM and logs. Comments updated to reflect Windows rationale.

Sequence Diagram

sequenceDiagram
    participant PM as ProcessManager
    participant Child as Child Process
    participant Stdin as stdin
    participant Taskkill as taskkill
    participant Signal as System Signal

    rect rgba(200,150,255,0.5)
    Note over PM,Child: Interrupt Phase (attempt graceful stop)
    alt Windows & child spawned in shell
        PM->>Child: write Ctrl+C to stdin
        Stdin-->>PM: unavailable? log warning
    else Non-Windows or no stdin
        PM->>Signal: send SIGINT
    end
    end

    rect rgba(255,180,100,0.5)
    Note over PM: Escalation on timeout
    PM->>PM: update message -> "Process did not exit after interrupt, escalating to kill"
    end

    rect rgba(255,150,150,0.5)
    Note over PM,Taskkill: Kill Phase (Windows)
    alt Windows & PID available
        PM->>Taskkill: execFile taskkill /pid ... /T /F
        Taskkill->>Child: terminate process tree
    end
    end

    rect rgba(150,200,150,0.5)
    Note over PM,Signal: Kill Phase (Non-Windows or fallback)
    PM->>Signal: send SIGTERM
    Signal->>Child: terminate process
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately describes the main fix: resolving the stop button malfunction on Windows for Claude Code agents, which is the core objective of the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Mar 11, 2026

Greptile Summary

This PR fixes the stop button not working on Windows for Claude Code agents by replacing unreliable POSIX signal delivery with Windows-native process termination mechanisms. It adds a \x03 (Ctrl+C) stdin write for graceful interrupts and taskkill /pid /t /f for forceful kills, while leaving Unix/macOS behavior untouched.

Key changes:

  • interrupt(): On Windows, writes \x03 to child.stdin instead of child.kill('SIGINT'), with a 2-second escalation to kill() if the process survives
  • kill(): On Windows, calls killWindowsProcessTree() using taskkill /pid /t /f to terminate the full process tree
  • killWindowsProcessTree(): New private method using execFile (async, no shell) to safely invoke taskkill
  • Uses execFile with an args array to prevent command injection

Issues found:

  • The child.stdin.destroyed guard does not cover streams that have been ended via stdin.end() (which happens for all batch-mode processes in ChildProcessSpawner). writableEnded should also be checked, otherwise write('\x03') silently fails for batch processes and the 2-second escalation always fires.
  • When pid is undefined on Windows, the code falls back to child.kill('SIGTERM') — the very approach this PR aims to replace — without any warning log to indicate the fallback occurred.

Confidence Score: 3/5

  • Safe to merge for interactive-mode agents; batch-mode Windows interrupts will silently fall back to the 2-second escalation path due to the writableEnded gap.
  • The core approach is sound and the Unix path is unchanged. However, the stdin.writableEnded oversight means the Ctrl+C soft-interrupt will silently fail for batch-mode processes on Windows (stdin is ended during spawn), always requiring the 2-second escalation. The pid-undefined Windows SIGTERM fallback is a minor inconsistency. Neither issue breaks correctness — the escalation timer provides a safety net — but the intended graceful interrupt won't work as advertised for one common usage pattern.
  • src/main/process-manager/ProcessManager.ts — specifically the interrupt() stdin guard and the kill() Windows fallback path

Important Files Changed

Filename Overview
src/main/process-manager/ProcessManager.ts Adds Windows-specific interrupt/kill logic using Ctrl+C stdin write and taskkill /t /f; two issues found: the stdin.destroyed check misses ended streams (batch mode processes will silently fail to receive Ctrl+C), and the pid-undefined fallback on Windows still uses unreliable SIGTERM.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[interrupt called] --> B{isTerminal / ptyProcess?}
    B -- yes --> C[ptyProcess.write Ctrl+C\nReturn true]
    B -- no --> D{childProcess exists?}
    D -- no --> E[Return false]
    D -- yes --> F{isWindows?}
    F -- no --> G[child.kill SIGINT]
    F -- yes --> H{stdin available\nAND not destroyed\nAND not writableEnded?}
    H -- yes --> I[stdin.write Ctrl+C\nLog debug]
    H -- no --> J[Log warn:\nstdin unavailable]
    G --> K[Set 2s escalation timer]
    I --> K
    J --> K
    K --> L{child exits\nbefore timer?}
    L -- yes --> M[clearTimeout\nNo further action]
    L -- no --> N[kill called]
    N --> O{isTerminal / ptyProcess?}
    O -- yes --> P[ptyProcess.kill]
    O -- no --> Q{isWindows AND pid?}
    Q -- yes --> R[killWindowsProcessTree\ntaskkill /pid /t /f\nasync execFile]
    Q -- no --> S[child.kill SIGTERM]
    R --> T[processes.delete\nReturn true]
    S --> T
    P --> T
Loading

Last reviewed commit: 9567c7f

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/main/process-manager/ProcessManager.ts (1)

233-249: Consider distinguishing genuine taskkill failures from expected ones.

The debug-level logging for errors is reasonable since taskkill returns non-zero when the process is already dead. However, genuine failures (access denied, taskkill not found on PATH, etc.) will also be logged at debug level and may go unnoticed.

Consider checking the error code/message to log genuine failures at warn level:

♻️ Optional: differentiate error types
 	private killWindowsProcessTree(pid: number, sessionId: string): void {
 		logger.info(
 			'[ProcessManager] Using taskkill to terminate process tree on Windows',
 			'ProcessManager',
 			{ sessionId, pid }
 		);
 		execFile('taskkill', ['/pid', String(pid), '/t', '/f'], (error) => {
 			if (error) {
-				// taskkill returns non-zero if the process is already dead, which is fine
-				logger.debug(
-					'[ProcessManager] taskkill exited with error (process may already be terminated)',
-					'ProcessManager',
-					{ sessionId, pid, error: String(error) }
-				);
+				// taskkill exit code 128 means process not found (already dead), which is expected
+				const isExpectedError = error.message?.includes('not found') || 
+					(error as NodeJS.ErrnoException).code === 'ENOENT';
+				const logFn = isExpectedError ? logger.debug : logger.warn;
+				logFn(
+					'[ProcessManager] taskkill exited with error',
+					'ProcessManager',
+					{ sessionId, pid, error: String(error), expected: isExpectedError }
+				);
 			}
 		});
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/process-manager/ProcessManager.ts` around lines 233 - 249, In
killWindowsProcessTree, the execFile callback currently logs all taskkill errors
at debug level; change it to inspect the Error object returned by execFile (in
the callback inside killWindowsProcessTree) and classify expected "process
already terminated" responses versus real failures: if error.message or
error.code indicates benign conditions (e.g., contains phrases like "not
running", "no such process", "not found", or exit/status codes known to mean
"already terminated") keep logger.debug, but if error indicates genuine failures
(e.g., ENOENT/taskkill not on PATH, "Access is denied", other non-benign exit
codes or messages) log with logger.warn and include sessionId, pid and the full
error details; keep the existing debug branch for the benign case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/process-manager/ProcessManager.ts`:
- Around line 233-249: In killWindowsProcessTree, the execFile callback
currently logs all taskkill errors at debug level; change it to inspect the
Error object returned by execFile (in the callback inside
killWindowsProcessTree) and classify expected "process already terminated"
responses versus real failures: if error.message or error.code indicates benign
conditions (e.g., contains phrases like "not running", "no such process", "not
found", or exit/status codes known to mean "already terminated") keep
logger.debug, but if error indicates genuine failures (e.g., ENOENT/taskkill not
on PATH, "Access is denied", other non-benign exit codes or messages) log with
logger.warn and include sessionId, pid and the full error details; keep the
existing debug branch for the benign case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 334eb72c-c480-4a5c-bc73-80e1e9409508

📥 Commits

Reviewing files that changed from the base of the PR and between 3e0710e and 9567c7f.

📒 Files selected for processing (1)
  • src/main/process-manager/ProcessManager.ts

- Check writableEnded on stdin before writing Ctrl+C to avoid
  ERR_STREAM_WRITE_AFTER_END on batch-mode processes where stdin
  was already ended via .end()
- Add warn log when pid is unavailable on Windows, making the
  SIGTERM fallback explicit rather than silent
@jSydorowicz21
Copy link
Author

Greptile concerns addressed, should be good to go

@jSydorowicz21
Copy link
Author

Test Failures addressed in #565

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant