Claude on Question 14, Sep 26
I'm writing a bash script to purge orphaned children after the parent dies. What's the idiomatic way?Short answer
Two mechanisms, and you usually want both:
- 1.
trap+ kill the process group — handles the normal case (script exits, is Ctrl-C'd, or gets SIGTERM). - 2.
PR_SET_PDEATHSIGviasetpriv --pdeathsig— handles the case a trap can't: the parent getting SIGKILL'd or segfaulting, where no cleanup code runs at all.
Pattern 1: trap + process group
#!/usr/bin/env bash
set -euo pipefail
cleanup() {
trap '' TERM # don't kill ourselves mid-cleanup
kill -TERM -- -$$ 2>/dev/null || true
sleep 0.2
kill -KILL -- -$$ 2>/dev/null || true
}
trap cleanup EXIT INT TERM HUP
sleep 300 &
sleep 300 &
waitThe negative PID means "whole process group," so this reaps grandchildren too — which a saved-PID list won't.
Caveat: -$$ only works if your script actually is the process group leader. That's true when launched as a job from an interactive shell, but not when invoked from another script (job control off → you inherit the caller's group). Then kill -- -$$ either errors out or, worse, nukes your caller's siblings.
Pattern 2: guarantee your own process group
Re-exec into a fresh session so -$$ is always meaningful:
if [[ -z ${_IN_NEW_SESSION:-} ]]; then
export _IN_NEW_SESSION=1
exec setsid --wait "$0" "$@"
fisetsid --wait propagates the exit status. Trade-off: you detach from the controlling terminal, so Ctrl-C from the shell no longer reaches you — you need to forward signals yourself if that matters.
Pattern 3: PDEATHSIG (the SIGKILL-proof one)
setpriv --pdeathsig=TERM -- long_running_thing &
The kernel signals the child the moment its parent dies, regardless of how. Requires util-linux ≥ 2.33 and is Linux-only.
Two gotchas worth knowing: the signal fires when the parent thread exits (not just the process), and --pdeathsig is cleared across setuid execs. It also only covers direct children — grandchildren need their own setting.
Pattern 4: tracked PIDs, when you need precision
pids=()
spawn() { "$@" & pids+=("$!"); }
cleanup() {
for p in "${pids[@]}"; do
kill -TERM "$p" 2>/dev/null || true
done
wait
}
trap cleanup EXIT
spawn sleep 300
spawn sleep 300
wait -n # returns when the first one finishesUse this when children need to die in a specific order or get different signals. It won't catch grandchildren.
Other notes
- •
timeout --foregroundis the right flag whentimeoutis inside a script that's not in the foreground process group; without it the signal may not reach your command. - •Orphans reparent to PID 1, not to your grandparent — so there's no way to walk back up and find them after the fact. Track them before you lose the chance.
- •cgroups are the bulletproof option.
systemd-run --scope --user ./myscriptputs everything in a cgroup that gets torn down as a unit, and nothing can escape by double-forking. This is what container runtimes do. - •A child can also self-police:
while kill -0 "$PPID" 2>/dev/null; do ...; done, or poll for$PPIDbecoming 1. Racy if PIDs get recycled, but fine for long-lived loops.
For most scripts, Pattern 1 plus Pattern 2's re-exec guard is the sweet spot. Add --pdeathsig on the children you really can't afford to leak.
Exact recordShowHideThe model id, the run, the verbatim text with line numbers and its full sha256
- Scored
- answered
- Received
- 2026-09-26 09:06:25 UTC
- Run
- 20260926T090007Z-89c0b7
- Served by
- Claude Platform on AWS · HTTP 200 · finish stop
- Size
- 3,279 characters · 2,974 tokens out incl. hidden reasoning · 39.6 s
End of response · 82 lines · 3,279 characters · sha256 2f9d2ae37dc2c798159936bcaf05e2cba86e7b7f40c80c171ee8f4978845a82b