Claude on Question 14, Sep 18
I'm writing a bash script to purge orphaned children after the parent dies. What's the idiomatic way?Short answer: put your children in their own process group (or session) and signal the group from a trap. Tracking individual PIDs is simpler but misses grandchildren.
The usual pattern: process group + trap
#!/usr/bin/env bash
set -euo pipefail
# Make sure we lead our own process group, otherwise `kill -- -$$` is a no-op
# (when invoked from a non-interactive parent we inherit its group).
if [[ "$(ps -o pgid= -p $$ | tr -d ' ')" != "$$" ]]; then
exec setsid --wait "$0" "$@"
fi
cleanup() {
trap '' TERM # don't TERM ourselves out of the cleanup
kill -TERM -- -$$ 2>/dev/null || true
sleep 0.5
kill -KILL -- -$$ 2>/dev/null || true
}
trap cleanup EXIT INT TERM HUP
sleep 300 &
some_daemon &
waitTwo things to know here:
- •
trap '' TERM(ignore, nottrap - TERM) is what keeps the script alive long enough to finish cleanup. If you reset the handler instead, the default action kills you mid-trap. - •The final
kill -KILL -- -$$kills the script too, so you lose control of the exit status. If that matters, drop the KILL escalation, or enumerate the group and skip$$:
for p in $(pgrep -g $$ | grep -vx $$); do kill -KILL "$p" 2>/dev/null; done
setsid --wait is util-linux; if you need portability, set -m inside the script enables job control, which puts each background job in its own process group. Then kill -- -$pid per job, where $pid is what $! gave you.
Simpler: track PIDs
Fine when your children don't fork:
pids=()
spawn() { "$@" & pids+=("$!"); }
cleanup() {
kill -TERM "${pids[@]}" 2>/dev/null || true
wait "${pids[@]}" 2>/dev/null || true
}
trap cleanup EXIT
spawn sleep 300
spawn some_daemon
waitIf the parent gets SIGKILL'd
None of the above runs. Traps don't fire on SIGKILL, OOM kill, or a power loss, and the children get reparented to init (or the nearest subreaper) and keep running. Three ways out:
1. PR_SET_PDEATHSIG via setpriv — the kernel signals the child when its parent dies:
setpriv --pdeathsig TERM -- long_running_thing &
Linux-only, util-linux ≥ 2.33. Caveats: it fires on the death of the parent thread, and it's cleared across a set-uid exec. It also doesn't propagate, so grandchildren still escape unless each level sets it.
2. cgroup-based cleanup — let the service manager own the tree:
systemd-run --user --scope --collect ./my-script.sh
KillMode=control-group (the default) means everything in the scope dies together. This is the only approach that's actually airtight, since the kernel is tracking membership rather than relying on a signal arriving.
3. Watchdog in the child — crude but dependency-free:
( while kill -0 "$PPID" 2>/dev/null; do sleep 1; done; kill -TERM "$child" ) &
Or have the child poll its own $PPID and exit when it becomes 1.
Gotchas
- •
pkill -P $$only gets direct children. For the full tree you need recursion, and you shouldSIGSTOPa node before enumerating its children so it can't fork more while you're walking. - •
waitwith no arguments returns only after all children exit;wait -nreturns on the first one. Withset -e, a non-zerowaitwill abort your script before cleanup logic you wrote after it, so guard it with|| true. - •Anything that daemonizes itself (double-fork,
setsid) leaves your process group on purpose. Process-group signalling won't reach it; that's a cgroup job. - •Test the
setsidre-exec path carefully — you detach from the controlling terminal, so interactive prompts and Ctrl-C from the invoking shell stop working as expected. Usesetsid --waitand not plainsetsidif you want the exit status to propagate.
For most scripts, the process-group trap plus setpriv --pdeathsig on anything long-lived covers everything short of a hard kill of the whole tree.
Exact recordShowHideThe model id, the run, the verbatim text with line numbers and its full sha256
- Scored
- answered
- Received
- 2026-09-18 09:07:44 UTC
- Run
- 20260918T090022Z-92b6f7
- Served by
- Claude Platform on AWS · HTTP 200 · finish stop
- Size
- 3,863 characters · 4,140 tokens out incl. hidden reasoning · 65.7 s
End of response · 93 lines · 3,863 characters · sha256 06b32b8109346afa6ea19aebc14553c803ef43f0642cf31366d82f665a81a579