diff --git a/packages/claude-code/lib/termux-run-claude-native.sh b/packages/claude-code/lib/termux-run-claude-native.sh index f14d79c..bd7738a 100755 --- a/packages/claude-code/lib/termux-run-claude-native.sh +++ b/packages/claude-code/lib/termux-run-claude-native.sh @@ -623,6 +623,7 @@ async function main() { const originalArgv = process.argv.slice(); const originalExit = process.exit; + const originalKill = process.kill; const originalBun = process.versions.bun; const hadGlobalBun = Object.prototype.hasOwnProperty.call(globalThis, 'Bun'); const originalGlobalBun = globalThis.Bun; @@ -700,8 +701,16 @@ async function main() { globalThis.__claudeBun = globalThis.__claudeBunShim; globalThis.Bun = globalThis.__claudeBunShim; process.argv = ['node', extractedFile, ...argv]; + let lastExitAttemptCode = 0; process.exit = code => { - throw new RequestedExit(code); + lastExitAttemptCode = code ?? 0; + throw new RequestedExit(lastExitAttemptCode); + }; + process.kill = (pid, signal) => { + if (pid === process.pid && (signal === 'SIGKILL' || signal === 9)) { + throw new RequestedExit(lastExitAttemptCode); + } + return originalKill.call(process, pid, signal); }; const moduleLike = { exports: {} }; const maybePromise = fn(moduleLike.exports, fakeRequire, moduleLike, extractedFile, workdir); @@ -725,6 +734,7 @@ async function main() { } process.argv = originalArgv; process.exit = originalExit; + process.kill = originalKill; try { if (originalBun === undefined) { delete process.versions.bun; @@ -1320,6 +1330,7 @@ async function main() { const originalArgv = process.argv.slice(); const originalExit = process.exit; + const originalKill = process.kill; const originalBun = process.versions.bun; const hadGlobalBun = Object.prototype.hasOwnProperty.call(globalThis, 'Bun'); const originalGlobalBun = globalThis.Bun; @@ -1398,8 +1409,16 @@ async function main() { globalThis.__claudeBun = globalThis.__claudeBunShim; globalThis.Bun = globalThis.__claudeBunShim; process.argv = ['node', extractedFile, ...argv]; + let lastExitAttemptCode = 0; process.exit = code => { - throw new RequestedExit(code); + lastExitAttemptCode = code ?? 0; + throw new RequestedExit(lastExitAttemptCode); + }; + process.kill = (pid, signal) => { + if (pid === process.pid && (signal === 'SIGKILL' || signal === 9)) { + throw new RequestedExit(lastExitAttemptCode); + } + return originalKill.call(process, pid, signal); }; const moduleLike = { exports: {} }; @@ -1419,6 +1438,7 @@ async function main() { process.removeListener('unhandledRejection', onAsyncError); process.argv = originalArgv; process.exit = originalExit; + process.kill = originalKill; process.once('exit', () => { if (extractedFile) { try { diff --git a/packages/claude-code/lib/termux-run-claude-native.test.js b/packages/claude-code/lib/termux-run-claude-native.test.js index adc5e30..609360a 100644 --- a/packages/claude-code/lib/termux-run-claude-native.test.js +++ b/packages/claude-code/lib/termux-run-claude-native.test.js @@ -577,6 +577,39 @@ function buildScenarioFixtureSource() { setTimeout(() => { process.stdout.write('ok'); }, 100).unref(); return; } + if (scenario === 'self-sigkill-fallback-string') { + try { + process.exit(17); + } catch (e) { + process.kill(process.pid, 'SIGKILL'); + } + return; + } + if (scenario === 'self-sigkill-fallback-numeric') { + try { + process.exit(17); + } catch (e) { + process.kill(process.pid, 9); + } + return; + } + if (scenario === 'self-sigkill-fallback-no-code') { + try { + process.exit(); + } catch (e) { + process.kill(process.pid, 'SIGKILL'); + } + return; + } + if (scenario === 'other-process-kill') { + const cp = require('child_process'); + const child = cp.spawn('node', ['-e', 'setTimeout(() => {}, 5000)']); + const childPid = child.pid; + process.kill(childPid, 0); + child.kill(); + process.stdout.write('ok'); + return; + } process.stdout.write('ok'); }`; } @@ -620,6 +653,45 @@ function runScenario({ printMode, stdinInherit, scenario }) { return { ...result, elapsedMs }; } +test('helper and bootstrap intercept process.kill(self, SIGKILL) statically', () => { + const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS='); + const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS='); + + // Check helper branch + const helperOriginalKillIdx = helperBlock.indexOf('const originalKill = process.kill;'); + assert.ok(helperOriginalKillIdx > -1, 'helper: missing originalKill declaration'); + + const helperProcessKillIdx = helperBlock.indexOf('process.kill = (pid, signal) => {', helperOriginalKillIdx); + assert.ok(helperProcessKillIdx > helperOriginalKillIdx, 'helper: process.kill override must come after originalKill declaration'); + + const helperRestoreIdx = helperBlock.indexOf('process.kill = originalKill;', helperProcessKillIdx); + assert.ok(helperRestoreIdx > helperProcessKillIdx, 'helper: process.kill restoration must come after override'); + + // Verify SIGKILL check within the override + const helperKillOverrideEnd = helperBlock.indexOf('};', helperProcessKillIdx); + const helperKillOverride = helperBlock.slice(helperProcessKillIdx, helperKillOverrideEnd); + assert.ok(helperKillOverride.includes('signal === \'SIGKILL\''), 'helper: process.kill must check for SIGKILL string'); + assert.ok(helperKillOverride.includes('signal === 9'), 'helper: process.kill must check for SIGKILL numeric value'); + assert.ok(helperKillOverride.includes('throw new RequestedExit'), 'helper: process.kill must throw RequestedExit for self SIGKILL'); + + // Check bootstrap branch + const bootstrapOriginalKillIdx = bootstrapBlock.indexOf('const originalKill = process.kill;'); + assert.ok(bootstrapOriginalKillIdx > -1, 'bootstrap: missing originalKill declaration'); + + const bootstrapProcessKillIdx = bootstrapBlock.indexOf('process.kill = (pid, signal) => {', bootstrapOriginalKillIdx); + assert.ok(bootstrapProcessKillIdx > bootstrapOriginalKillIdx, 'bootstrap: process.kill override must come after originalKill declaration'); + + const bootstrapRestoreIdx = bootstrapBlock.indexOf('process.kill = originalKill;', bootstrapProcessKillIdx); + assert.ok(bootstrapRestoreIdx > bootstrapProcessKillIdx, 'bootstrap: process.kill restoration must come after override'); + + // Verify SIGKILL check within the override + const bootstrapKillOverrideEnd = bootstrapBlock.indexOf('};', bootstrapProcessKillIdx); + const bootstrapKillOverride = bootstrapBlock.slice(bootstrapProcessKillIdx, bootstrapKillOverrideEnd); + assert.ok(bootstrapKillOverride.includes('signal === \'SIGKILL\''), 'bootstrap: process.kill must check for SIGKILL string'); + assert.ok(bootstrapKillOverride.includes('signal === 9'), 'bootstrap: process.kill must check for SIGKILL numeric value'); + assert.ok(bootstrapKillOverride.includes('throw new RequestedExit'), 'bootstrap: process.kill must throw RequestedExit for self SIGKILL'); +}); + test('helper branch (CLI -p, no stdin inherit): normal/sync-exit/async-exit all produce output', () => { for (const scenario of ['normal', 'sync-exit', 'async-exit']) { const r = runScenario({ printMode: true, stdinInherit: false, scenario }); @@ -638,3 +710,27 @@ test('bootstrap branch (-p + CLAUDE_TERMUX_STDIN=inherit): normal/sync-exit/asyn } } }); + +test('helper branch intercepts self-directed SIGKILL (string signal) and exits with proper code', () => { + const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'self-sigkill-fallback-string' }); + assert.equal(r.status, 17, `expected status 17, got ${r.status}; stderr=${r.stderr}`); + assert.ok(!r.signal, `expected clean exit (no signal), but got signal: ${r.signal}`); +}); + +test('helper branch intercepts self-directed SIGKILL (numeric signal 9) and exits with proper code', () => { + const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'self-sigkill-fallback-numeric' }); + assert.equal(r.status, 17, `expected status 17, got ${r.status}; stderr=${r.stderr}`); + assert.ok(!r.signal, `expected clean exit (no signal), but got signal: ${r.signal}`); +}); + +test('helper branch intercepts self-directed SIGKILL with process.exit() (no code) and defaults to 0', () => { + const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'self-sigkill-fallback-no-code' }); + assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`); + assert.ok(!r.signal, `expected clean exit (no signal), but got signal: ${r.signal}`); +}); + +test('helper branch allows process.kill to other processes (signal 0, passthrough)', () => { + const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'other-process-kill' }); + assert.ok((r.stdout || '').includes('ok'), `expected ok output, got stdout=${r.stdout} stderr=${r.stderr}`); + assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`); +});