Skip to content

Commit 8e14cfa

Browse files
michelhelsdingenclaudehappy-otter
committed
fix(mode-switch): signal forwarding prevents orphaned processes (slopus#11)
## THE FIX Forward SIGTERM, SIGINT, SIGHUP to child Claude CLI process. This ensures child processes are killed when parent is terminated. ## ROOT CAUSE When switching between local/remote modes, the parent process was killed but child Claude CLI processes remained alive ("orphaned"). These orphaned processes continued to hold stdin, causing: - Duplicate/garbled characters when typing - "Competing processes" fighting for terminal input ## SOLUTION (from GitHub slopus/happy#430) ```javascript const forwardSignal = (signal) => { if (child.pid && !child.killed) { child.kill(signal); } }; process.on('SIGTERM', () => forwardSignal('SIGTERM')); process.on('SIGINT', () => forwardSignal('SIGINT')); process.on('SIGHUP', () => forwardSignal('SIGHUP')); ``` ## FILES CHANGED - scripts/claude_version_utils.cjs - binary launcher signal forwarding - src/claude/claudeLocal.ts - TypeScript launcher signal forwarding ## TESTED ✅ Fresh local mode - typing works ✅ Switch to remote mode - typing works ✅ Switch back to local mode - typing works (was broken) ✅ Multiple mode switches - stable ## IMPORTANT This is the ONLY fix needed. No stdin cleanup, no removeAllListeners(), no setRawMode changes required. Signal forwarding alone solves it. Closes slopus#11 References: slopus/happy#430, PR slopus#127 Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
1 parent e2da751 commit 8e14cfa

3 files changed

Lines changed: 37 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "happy-coder",
3-
"version": "0.14.0-0",
3+
"version": "0.14.1-0",
44
"description": "Mobile and Web client for Claude Code and Codex",
55
"author": "Kirill Dubovitskiy",
66
"license": "MIT",

scripts/claude_version_utils.cjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,19 @@ function runClaudeCli(cliPath) {
497497
stdio: 'inherit',
498498
env: process.env
499499
});
500+
501+
// Forward signals to child process so it gets killed when parent is killed
502+
// This prevents orphaned Claude processes when switching between local/remote modes
503+
// Fix for issue #11 / GitHub slopus/happy#430
504+
const forwardSignal = (signal) => {
505+
if (child.pid && !child.killed) {
506+
child.kill(signal);
507+
}
508+
};
509+
process.on('SIGTERM', () => forwardSignal('SIGTERM'));
510+
process.on('SIGINT', () => forwardSignal('SIGINT'));
511+
process.on('SIGHUP', () => forwardSignal('SIGHUP'));
512+
500513
child.on('exit', (code) => {
501514
process.exit(code || 0);
502515
});

src/claude/claudeLocal.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,29 @@ export async function claudeLocal(opts: {
232232
env,
233233
});
234234

235+
// Forward signals to child process to prevent orphaned processes
236+
// Fix for issue #11 / GitHub slopus/happy#430
237+
// Note: signal: opts.abort handles programmatic abort (mode switching),
238+
// but direct OS signals (e.g., kill, Ctrl+C) need explicit forwarding
239+
const forwardSignal = (signal: NodeJS.Signals) => {
240+
if (child.pid && !child.killed) {
241+
child.kill(signal);
242+
}
243+
};
244+
const onSigterm = () => forwardSignal('SIGTERM');
245+
const onSigint = () => forwardSignal('SIGINT');
246+
const onSighup = () => forwardSignal('SIGHUP');
247+
process.on('SIGTERM', onSigterm);
248+
process.on('SIGINT', onSigint);
249+
process.on('SIGHUP', onSighup);
250+
251+
// Cleanup signal handlers when child exits to avoid leaks
252+
child.on('exit', () => {
253+
process.off('SIGTERM', onSigterm);
254+
process.off('SIGINT', onSigint);
255+
process.off('SIGHUP', onSighup);
256+
});
257+
235258
// Listen to the custom fd (fd 3) for thinking state tracking
236259
if (child.stdio[3]) {
237260
const rl = createInterface({

0 commit comments

Comments
 (0)