Ctrl+C (SIGINT/SIGTERM) during a running command crashes with an uncaught ExitError instead of exiting cleanly
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- node.js, typescript
- Domain
- cli
Research direction
Start in src/sfCommand.ts at the signal handlers registered by SfCommand._run(), then reproduce the issue with a long-running SfCommand and Ctrl+C. Ensure the signal path exits cleanly with code 130 rather than producing an uncaught ExitError and stack trace.
Written by the indexing model from the issue text.
Description
Summary
SfCommand._run() registers a handler for SIGINT/SIGTERM/SIGBREAK/SIGHUP that calls this.exit(130). Command.exit() (from @oclif/core) just does throw new ExitError(code) — it relies on being caught by oclif's own command-execution wrapper. But since the signal handler runs outside that wrapper's call stack, the thrown ExitError is never caught anywhere and Node treats it as an uncaught exception, crashing the process with a raw stack trace instead of the clean exit 130 this code is clearly trying to produce.
This reproduces on the current main branch (confirmed by reading src/sfCommand.ts), not just the version we have pinned (7.1.16):
// src/sfCommand.ts
public async _run<R>(): Promise<R> {
['SIGINT', 'SIGTERM', 'SIGBREAK', 'SIGHUP'].map((listener) => {
process.on(listener, () => {
this.exit(130);
});
});
...
// @oclif/core: lib/command.js
exit(code = 0) {
Errors.exit(code);
}
// @oclif/core: lib/errors/index.js
function exit(code = 0) {
throw new exit_1.ExitError(code);
}
Steps to reproduce
- Any
SfCommandsubclass whoserun()takes more than a moment (e.g. anawaiton a network call, a poll loop, etc.) - Run the command
- Press Ctrl+C while it's running
Expected behavior
The process exits cleanly with code 130 (as the code's own this.exit(130) call clearly intends).
Actual behavior
Uncaught exception, raw stack trace printed, no clean shutdown:
ExitError: EEXIT: 130
at Object.exit (@oclif/core/lib/errors/index.js:20:11)
at Deploy.exit (@oclif/core/lib/command.js:180:16)
at process.<anonymous> (@salesforce/sf-plugins-core/lib/sfCommand.js:262:22)
at process.emit (node:events:530:35) {
code: 'EEXIT',
oclif: { exit: 130 },
skipOclifErrorHandling: undefined,
suggestions: undefined
}
Node.js v22.18.0
(Captured from a real command built on @salesforce/sf-plugins-core@7.1.16 + @oclif/core@3.27.0, Node v22.18.0, Windows.)
Environment
@salesforce/sf-plugins-core: 7.1.16 (bug also present inmain/13.0.3 source, see above)@oclif/core: 3.27.0- Node.js: v22.18.0
- OS: Windows 11
Suggested fix
Wrap the signal-handler body so the throw is actually caught, e.g.:
process.on(listener, () => {
try {
this.exit(130);
} catch {
process.exit(130);
}
});
or avoid the throw-based exit() path entirely for this specific case and call process.exit(130) directly, since there's no command-completion promise chain to unwind through here anyway.
Workaround we're using in the meantime
In our plugin we replace these listeners with our own (process.removeAllListeners(signal) then re-register) so we can also request cancellation of an in-flight external operation before exiting. Happy to share the exact code if useful, but it's plugin-specific beyond the core fix above.
- Dominant language
- TypeScript
- Stars
- 18
- Forks
- 5
- Avg merge
- 37m
- Merged PRs (30d)
- 3
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Similar issues
-
clawsweeper:fix-shape-clear clawsweeper:queueable-fix clawsweeper:source-repro impact:ux-friction issue-rating: 🦞 diamond lobster no-stale P3
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Difficulty 1/5 Under an hour Newbie friendliness 76/100
-
code-quality refactoring
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
github/gh-aw-firewall#8816 ·
-
integration:quickjs org:external priority:backlog topic:code-interpreter topic:middleware type:feature
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
langchain-ai/deepagents#6450 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100
vercel/react-tweet#225 ·