Bash trap Command Not Catching SIGINT or SIGTERM in Subshells
Quick answer
A trap handler that works correctly in the main script but doesn't fire inside a subshell or background process is expected Bash behavior — signal handling in...
A trap handler that works correctly in the main script but doesn't fire inside a subshell or background process is expected Bash behavior — signal handling in subshells doesn't automatically inherit the parent shell's trap configuration, and background processes specifically require their own explicit signal forwarding to respond to signals sent to the parent script at all.
The Problem
A script sets up a trap to handle cleanup on interruption, but the handler doesn't fire when expected, particularly when the interrupted code is running inside a subshell or as a background process:
trap 'echo "Cleaning up"; exit' SIGINT SIGTERM
(
sleep 100 # this subshell doesn't respond to the parent's trap the same way
)
Pressing Ctrl+C (sending SIGINT) while the subshell is running might not trigger the expected cleanup message, or might behave inconsistently depending on exactly how the subshell and signal delivery interact.
Why It Happens
Several distinct behaviors around subshells, background processes, and signal handling commonly cause traps to not fire as expected:
- A subshell (created with parentheses
( )) runs as a genuinely separate process, and while it does inherit trap settings from the parent shell at the time it's created, certain signal behaviors and timing can still produce unexpected results depending on exactly when and how the signal is delivered - A background process (started with
&) is even more independent — it doesn't automatically receive signals sent to the parent script's process at all, since it's running as its own separate process in the background, requiring explicit signal forwarding if you want it to respond to the same signals the parent receives - A command run inside a pipeline creates its own subshell context in Bash (for each stage of the pipeline), which can have its own signal handling nuances distinct from the main script's context
The Fix
For background processes specifically, explicitly forward signals from the parent script to the background process, since this doesn't happen automatically:
cleanup() {
echo "Cleaning up"
kill "$child_pid" 2>/dev/null
exit
}
trap cleanup SIGINT SIGTERM
sleep 100 &
child_pid=$!
wait "$child_pid"
This pattern explicitly captures the background process's PID, and the trap handler explicitly sends a signal to that specific child process when the parent script itself receives SIGINT/SIGTERM — without this explicit forwarding, the background process has no inherent connection to signals the parent script receives.
For a subshell specifically, if you need the subshell's own trap behavior to be genuinely independent (running its own distinct cleanup logic), define the trap explicitly within the subshell itself, rather than assuming it automatically inherits and correctly applies the parent's trap configuration in every situation:
(
trap 'echo "Subshell cleanup"; exit' SIGINT SIGTERM
sleep 100
)
Defining the trap explicitly inside the subshell removes any ambiguity about inheritance behavior, giving that subshell its own clear, directly-defined signal handling rather than relying on it correctly inheriting the parent's configuration in every circumstance.
For a pipeline, be aware that each command in the pipeline runs as its own process, and signal delivery/handling can behave differently than a simple, single foreground command — if you need specific signal handling within a pipeline stage, that specific command needs its own explicit trap or signal handling logic, since the parent script's own trap doesn't automatically and reliably extend into every pipeline stage's own separate process:
your_command | while read -r line; do
# this subshell (from the pipe) needs its own trap if specific cleanup is needed here
trap 'echo "cleaning up in loop"; exit' SIGINT
process "$line"
done
For more complex scripts spawning multiple background processes that all need to be cleaned up together on interruption, track every child PID explicitly and terminate them all in the trap handler, rather than relying on any automatic signal propagation to handle this for you:
pids=()
cleanup() {
echo "Terminating all child processes"
for pid in "${pids[@]}"; do
kill "$pid" 2>/dev/null
done
exit
}
trap cleanup SIGINT SIGTERM
command1 &
pids+=($!)
command2 &
pids+=($!)
wait
Still Not Working?
If explicit signal forwarding still doesn't produce the expected cleanup behavior, check whether the specific command running in the background or subshell has its own signal handling that's interfering with or overriding the default behavior — some programs install their own signal handlers that change how they respond to SIGTERM/SIGINT specifically (some, for example, ignore SIGTERM by default and require SIGKILL, or require a specific different signal to trigger graceful shutdown), meaning even correctly forwarded signals might not produce the expected termination behavior if the target program's own internal signal handling doesn't respond the way you assumed it would.