DEV-13Recommended
Clean up and exit with 128 plus the signal number when stopped
On SIGINT or SIGTERM, start no new work, delete temporary and lock files, restore the terminal and exit with 128 plus the signal number. A second Ctrl+C exits at once.
How to do it
A tool stopped halfway leaves lock files that block the next run, temporary files that fill the disk and a terminal with no cursor. POSIX only says a command killed by a signal reports a status greater than 128. Shells such as bash report 128 plus the signal number, so callers expect 130 for SIGINT and 143 for SIGTERM. Write output to a temporary name and rename it only when the work is done, so an interrupted run never leaves half a file. Keep cleanup under 5 seconds. A person who presses Ctrl+C twice wants out now, so the second signal skips cleanup. The exit codes for other failures follow Exit with zero only when the command did everything it was asked. Long-running servers stop as shut down without dropping requests says.
Examples
Python example
import os, shutil, signal, sys, tempfile
SIGNAL_EXIT_BASE = 128
stopping = False
def on_signal(signum, _frame):
global stopping
if stopping:
os._exit(SIGNAL_EXIT_BASE + signum)
stopping = True
raise SystemExit(SIGNAL_EXIT_BASE + signum)
signal.signal(signal.SIGINT, on_signal)
signal.signal(signal.SIGTERM, on_signal)
work_dir = tempfile.mkdtemp(prefix="example-")
try:
run(work_dir)
finally:
shutil.rmtree(work_dir, ignore_errors=True)
sys.stderr.write("\x1b[?25h")
Go example
const signalExitBase = 128
func main() {
workDir, err := os.MkdirTemp("", "example-")
if err != nil {
os.Exit(1)
}
signals := make(chan os.Signal, 2)
signal.Notify(signals, os.Interrupt, syscall.SIGTERM)
ctx, cancel := context.WithCancel(context.Background())
var received atomic.Int32
go func() {
first := (<-signals).(syscall.Signal)
received.Store(int32(first))
cancel()
<-signals
os.Exit(signalExitBase + int(first))
}()
runErr := run(ctx, workDir)
os.RemoveAll(workDir)
if sig := received.Load(); sig != 0 {
os.Exit(signalExitBase + int(sig))
}
if runErr != nil {
os.Exit(1)
}
}
How to check
A test starts a long command and sends SIGINT after 1 second. The tool exits 130, and no temporary file, lock file or partial output file remains. SIGTERM gives 143. Two SIGINT signals 100 ms apart end the process within 1 second.
Sources
Related rules
- DEV-1 Exit with zero only when the command did everything it was asked in Developer tools, SDKs and CLIs
- OPS-35 Shut down cleanly in Operations and observability
For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/developer-tools/command-line-behaviour/signal-cleanup">Agent Wranglers developer tools, SDKs and CLIs playbook</a>. In code you write, add a comment next to it with the same link.