When a user presses Ctrl+C mid-operation and the CLI has no SIGINT handler, any temporary files written during that run become orphaned, any terminal modifications (hidden cursor, raw mode, alternate screen) persist into the next shell prompt, and partial output files are left in a state that corrupts subsequent runs. ISO 25010:2011 reliability.fault-tolerance requires that interruption be a recoverable event, not a resource leak. The exit code matters too: a process killed by SIGINT that exits 0 misleads shell scripts into treating the interrupt as a successful completion — the conventional exit codes are 130 (SIGINT) and 143 (SIGTERM), encoding 128 + signal_number.
Medium because absent signal handling leaves orphaned resources and corrupted terminal state on every Ctrl+C, but the impact is bounded to the local user session.
Register cleanup handlers for both SIGINT and SIGTERM before any long-running work begins:
const tempFiles: string[] = []
process.on('SIGINT', () => {
console.error('\nInterrupted, cleaning up...')
for (const f of tempFiles) {
try { unlinkSync(f) } catch {}
}
process.exit(130) // 128 + 2 (SIGINT)
})
process.on('SIGTERM', () => {
console.error('Terminated, cleaning up...')
for (const f of tempFiles) {
try { unlinkSync(f) } catch {}
}
process.exit(143) // 128 + 15 (SIGTERM)
})
import signal, sys
def handle_sigint(signum, frame):
click.echo('\nInterrupted, cleaning up...', err=True)
cleanup()
sys.exit(130)
signal.signal(signal.SIGINT, handle_sigint)
If your CLI hides the cursor or enters raw mode, restore terminal state in the same handler before exiting.
ID: cli-quality.error-handling.signal-handling
Severity: medium
What to look for: Count all long-running operations and for each, check whether SIGINT/SIGTERM handlers are registered. check for SIGINT (Ctrl+C) and SIGTERM handling. Look for process.on('SIGINT', ...), signal.signal(signal.SIGINT, ...), signal.Notify(...) in Go, or ctrlc crate in Rust. Verify that signal handlers: (1) clean up temporary files/resources, (2) restore terminal state if modified, (3) exit with code 128+signal number (130 for SIGINT, 143 for SIGTERM). Check that long-running operations can be interrupted without leaving partial state.
Pass criteria: SIGINT and SIGTERM are handled with cleanup logic. Exit code is 130 for SIGINT and 143 for SIGTERM (or at minimum, non-zero). Temporary files and resources are cleaned up on signal. Terminal state is restored if the CLI modifies it (e.g., raw mode, hidden cursor) — at least 1 signal handler (SIGINT or SIGTERM) must be registered for graceful cleanup. Report: "X long-running operations found, Y handle SIGINT gracefully."
Fail criteria: No signal handling — Ctrl+C causes abrupt exit with potential resource leaks. Or signal handlers catch the signal but exit with code 0. Or terminal modifications (hidden cursor, raw mode) are not restored on interrupt.
Skip (N/A) when: The CLI performs only short, stateless operations — no temporary files, no terminal modifications, no long-running processes. All checks skip when no CLI entry point is detected.
Detail on fail: "No SIGINT handler — Ctrl+C during file processing may leave partial output files and a hidden cursor" or "SIGINT handler calls process.exit(0) instead of process.exit(130)"
Remediation: Proper signal handling prevents resource leaks and orphaned state:
// Node.js — signal handling with cleanup
let tempFiles: string[] = []
process.on('SIGINT', () => {
console.error('\nInterrupted, cleaning up...')
for (const f of tempFiles) {
try { unlinkSync(f) } catch {}
}
process.exit(130) // 128 + 2 (SIGINT)
})
process.on('SIGTERM', () => {
console.error('Terminated, cleaning up...')
for (const f of tempFiles) {
try { unlinkSync(f) } catch {}
}
process.exit(143) // 128 + 15 (SIGTERM)
})
# Python — signal handling
import signal, sys
def handle_sigint(signum, frame):
click.echo('\nInterrupted, cleaning up...', err=True)
cleanup()
sys.exit(130)
signal.signal(signal.SIGINT, handle_sigint)