Dark Mode

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

Copy link

jSydorowicz21 commented Mar 11, 2026 *
edited by coderabbitai bot
Loading

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.

greptile-apps[bot] reacted with thumbs up emoji
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.
Copy link

coderabbitai bot commented Mar 11, 2026 *
edited
Loading

No actionable comments were generated in the recent review.

i 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.

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

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 " dir="auto">
%%{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

greptile-apps bot reviewed Mar 11, 2026
coderabbitai bot reviewed Mar 11, 2026
Copy link

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
{ 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 } + ); } }); }"> 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.

i 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

coderabbitai bot approved these changes Mar 11, 2026
- 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
Copy link
Author

jSydorowicz21 commented Mar 11, 2026

Greptile concerns addressed, should be good to go

Copy link
Author

jSydorowicz21 commented Mar 13, 2026

Test Failures addressed in #565

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

Reviewers

greptile-apps[bot] greptile-apps[bot] left review comments

coderabbitai[bot] coderabbitai[bot] approved these changes

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant