Skip to content

Commit 9f2c5fb

Browse files
committed
fix: upgrade node-pty to 1.2.0-beta.11 to fix macOS PTY leak
node-pty v1.1.0 has an off-by-one bug in pty_posix_spawn() on macOS where low_fds[0] (allocated via posix_openpt()) is never closed due to a buggy cleanup loop (`count > 0` vs the correct `i <= count`). This leaks 1 PTY master fd per pty.spawn() call, eventually exhausting all 511 PTY pairs on macOS (kern.tty.ptmx_max). Upstream fix: microsoft/node-pty#882 (merged 2026-01-28). Also fixes the abort handler to kill the entire process group (-pid) with SIGTERM instead of sending SIGHUP to just the launcher PID, ensuring the Claude binary grandchild is also terminated.
1 parent aa5d706 commit 9f2c5fb

3 files changed

Lines changed: 22 additions & 4 deletions

File tree

packages/happy-cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
"tar": "^7.5.2",
116116
"tmp": "^0.2.5",
117117
"tweetnacl": "^1.0.3",
118-
"node-pty": "^1.1.0",
118+
"node-pty": "1.2.0-beta.11",
119119
"zod": "3.25.76"
120120
},
121121
"devDependencies": {

packages/happy-cli/src/claude/claudeLocal.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,17 @@ export async function claudeLocal(opts: {
317317
let ptyProcess: ReturnType<typeof pty.spawn> | null = null;
318318

319319
const abortHandler = () => {
320-
logger.debug('[ClaudeLocal] Abort signal triggered - terminating PTY process');
320+
logger.debug('[ClaudeLocal] Abort signal triggered - terminating PTY process group');
321321
try {
322-
ptyProcess?.kill();
322+
// Kill the entire process group (negative PID) so the Claude binary
323+
// grandchild is also terminated, not just the node launcher.
324+
// node-pty spawns with POSIX_SPAWN_SETSID so the child is its own
325+
// session leader and process group leader.
326+
if (ptyProcess?.pid) {
327+
process.kill(-ptyProcess.pid, 'SIGTERM');
328+
}
323329
} catch {
324-
// Already dead
330+
// Already dead or no such process group
325331
}
326332
};
327333
opts.abort.addEventListener('abort', abortHandler, { once: true });

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10251,6 +10251,11 @@ new-github-release-url@2.0.0:
1025110251
dependencies:
1025210252
type-fest "^2.5.1"
1025310253

10254+
node-addon-api@^7.1.0:
10255+
version "7.1.1"
10256+
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
10257+
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
10258+
1025410259
node-fetch-native@^1.6.6:
1025510260
version "1.6.7"
1025610261
resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz#9d09ca63066cc48423211ed4caf5d70075d76a71"
@@ -10273,6 +10278,13 @@ node-int64@^0.4.0:
1027310278
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
1027410279
integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==
1027510280

10281+
node-pty@1.2.0-beta.11:
10282+
version "1.2.0-beta.11"
10283+
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.2.0-beta.11.tgz#b12338927cd5da9cfba82e39b5b36f74e52cbbe1"
10284+
integrity sha512-THcUyu1WwdgoIyUvgXOZ70EOMXzheGa0q3tbEb5kUIfKgcpBJ+AJ9Q1kq0bKtYmQzr77usXiTORZTLmAUQlnoQ==
10285+
dependencies:
10286+
node-addon-api "^7.1.0"
10287+
1027610288
node-releases@^2.0.27:
1027710289
version "2.0.27"
1027810290
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e"

0 commit comments

Comments
 (0)