Claude on Question 14, Sep 22
I'm writing a bash script to purge orphaned children after the parent dies. What's the idiomatic way?There are three tiers of answer depending on how the parent dies.
1. Normal exits and catchable signals: trap + tracked PIDs
This is the idiomatic 90% solution. Record $! as you spawn, kill the list from an EXIT trap:
#!/usr/bin/env bash
set -euo pipefail
children=()
cleanup() {
trap - EXIT INT TERM # prevent re-entry
(( ${#children[@]} )) && kill "${children[@]}" 2>/dev/null
wait 2>/dev/null # reap, don't leave zombies
}
trap cleanup EXIT INT TERM
sleep 300 & children+=("$!")
sleep 300 & children+=("$!")
waitNote that bash runs the EXIT trap after the INT/TERM handlers return, so trapping all three and disarming inside cleanup avoids double-killing. Guard the array expansion because set -u will explode on an empty array in older bash.
2. If your children spawn their own children
Killing $! only signals the direct child; grandchildren get reparented and survive. Turn on job control so each background job becomes its own process-group leader, then signal the whole group with a negative PID:
set -m # each job gets its own pgid == $!
cleanup() {
trap - EXIT INT TERM
for p in "${children[@]}"; do
kill -TERM -- "-$p" 2>/dev/null # note the leading dash: process group
done
}The lazier variant is trap 'kill 0' EXIT, which signals your own process group. It works when the script was launched from an interactive shell (job control gave it a fresh pgid), but if the script was invoked from another script it inherits the caller's group and kill 0 will take out your parent too. I'd avoid it in anything reusable.
Caveat with set -m: you get job-completion chatter on stderr, and children are no longer in the terminal's foreground group, so anything that reads stdin from the tty gets SIGTTIN.
3. If the parent dies by SIGKILL or segfault
No trap runs, so you need the kernel to do the work. On Linux, setpriv exposes PR_SET_PDEATHSIG:
setpriv --pdeathsig TERM -- long_running_thing &
The kernel signals the child the moment its parent dies, however it dies. Requires util-linux ≥ 2.33. Two gotchas: the signal fires when the forking thread exits (fine for bash), and it's cleared across a setuid exec.
A portable fallback is a pipe-EOF watchdog — the child inherits a read end whose write end only the parent holds, so read returns EOF exactly when the parent disappears. Or poll, recording the original PPID rather than testing $PPID -eq 1 (under a container init or a subreaper, orphans don't get reparented to PID 1):
watchdog() {
local ppid=$1 target=$2
while kill -0 "$ppid" 2>/dev/null; do sleep 1; done
kill "$target" 2>/dev/null
}If you're on systemd and this is a service, skip all of it: systemd-run --scope and let KillMode=control-group clean up the cgroup, which is the only approach that's actually airtight.
Related gotcha
Orphans and zombies are different failures. An orphan is still running with a new parent; a zombie has exited but nobody called wait, so its exit status pins a slot in the process table. If your script is long-lived and spawns many children, wait on each one (or wait -n in a loop) or you'll leak PIDs regardless of your kill logic.
Exact recordShowHideThe model id, the run, the verbatim text with line numbers and its full sha256
- Scored
- answered
- Received
- 2026-09-22 09:07:36 UTC
- Run
- 20260922T090022Z-7ff13b
- Served by
- Claude Platform on AWS · HTTP 200 · finish stop
- Size
- 3,321 characters · 4,204 tokens out incl. hidden reasoning · 57.0 s
End of response · 71 lines · 3,321 characters · sha256 653378579caca033ec31b5ea752e7c0119a45f7f6ccd81aa3ef47facba931172