Skip to content
Shell & TerminalDeep Dive July 12, 2026 2 min readViews unavailable

Shell Signal Handling: trap, Cleanup, and Process Groups

Reliable shell cleanup requires understanding signals, traps, process groups, and the important difference between a normal exit and an uncatchable termination.

Shell scripts often create temporary files, start child processes, or partially update state. trap can make cleanup reliable, but only when the script distinguishes shell exit handling from Unix signal delivery.

A trap replaces the shell’s default action

cleanup() {
  rm -rf -- "$workdir"
}

workdir=$(mktemp -d) || exit 1
trap cleanup EXIT HUP INT TERM

EXIT is a shell pseudo-condition: the handler runs when the shell exits normally or after a caught signal leads to exit. HUP, INT, and TERM are real signals. Quoting the handler at trap-definition time delays variable expansion until it runs; using a function is easier to read and extend.

SIGKILL and SIGSTOP cannot be caught, blocked, or ignored. No trap can guarantee cleanup after kill -9, a kernel crash, or power loss. That is why temporary-state design still matters even when traps exist.

Ctrl-C targets a foreground process group

An interactive terminal does not simply send SIGINT to “the shell.” Its terminal driver sends the signal to the current foreground process group. When a script launches a foreground command, that command and potentially the script can both participate in the terminal job-control arrangement.

This explains why signal behavior can change when a command is placed in a pipeline or backgrounded. Shells also differ in some details around waiting for children and running traps, so portable scripts should keep handlers simple and test the actual shells they support.

Preserve the reason for termination

A cleanup handler that always exits zero can hide failure. One robust pattern captures the current status:

cleanup() {
  status=$?
  trap - EXIT HUP INT TERM
  rm -rf -- "$workdir"
  exit "$status"
}
trap cleanup EXIT HUP INT TERM

Resetting the traps prevents recursion. For applications that need conventional signal-derived statuses, separate handlers can clean up and then re-raise the original signal after restoring its default action.

Keep traps idempotent and narrow

A handler may run after only half the setup completed, so cleanup must tolerate missing files and already-stopped children. Avoid large amounts of business logic in signal handlers. Record a stop request, terminate owned children deliberately, remove private temporary data, and let the main flow decide whether retry or rollback is safe.

Most importantly, never use a broad command such as kill 0 without understanding the current process group: it can signal the calling shell or unrelated jobs sharing that group.

Sources: