Claude on Question 14, Sep 30
I'm writing a bash script to purge orphaned children after the parent dies. What's the idiomatic way?The short answer
Kill the process group from an EXIT trap:
#!/usr/bin/env bash set -euo pipefail # Reset the trap first so we don't recurse when the signal hits us too trap 'trap - TERM INT EXIT; kill -- -$$ 2>/dev/null' EXIT INT TERM sleep 300 & sleep 300 & wait
kill -- -$$ signals the whole group. Caveat: this only works if your script is the process-group leader. When you run it interactively from a shell with job control, it usually is. When it's invoked from another script (no job control), it inherits the caller's pgid and you'd nuke your parent's siblings too. So either:
# re-exec ourselves as a session/pgroup leader
[[ -z ${UNDER_SETSID:-} ]] && exec env UNDER_SETSID=1 setsid -w "$0" "$@"or enable job control and background the work so it lands in a fresh group:
set -m
If you want explicit control
Track PIDs yourself. More code, but no surprises:
#!/usr/bin/env bash
set -euo pipefail
pids=()
spawn() { "$@" & pids+=("$!"); }
cleanup() {
local pid
for pid in "${pids[@]}"; do
kill -TERM "$pid" 2>/dev/null || true
done
# give them a moment, then escalate
local deadline=$((SECONDS + 5))
while (( SECONDS < deadline )); do
kill -0 "${pids[@]}" 2>/dev/null || return 0
sleep 0.2
done
kill -KILL "${pids[@]}" 2>/dev/null || true
}
trap cleanup EXIT
spawn sleep 300
spawn sleep 300
waitNote that kill -TERM "$pid" only hits that one process — if your child spawns grandchildren, they survive. Use kill -- -"$pid" with set -m if you want each job in its own group and want the whole subtree to go.
The hard case: parent gets SIGKILL
Traps don't run on SIGKILL or a power loss, so anything above is best-effort. If you truly need guaranteed reaping, the child has to defend itself:
Linux PR_SET_PDEATHSIG — kernel kills the child when its parent dies:
pdeath() {
python3 -c 'import ctypes, os, sys
ctypes.CDLL("libc.so.6", use_errno=True).prctl(1, 15) # PR_SET_PDEATHSIG, SIGTERM
os.execvp(sys.argv[1], sys.argv[1:])' "$@"
}
pdeath sleep 300 &Careful: PDEATHSIG fires when the thread that forked exits, and it's cleared across setuid exec. Also, if the parent is already dead by the time prctl runs, you miss it — check $PPID right after setting it.
cgroups (most robust) — let the kernel own the lifecycle:
systemd-run --user --scope --collect -- ./my-script.sh
When the scope dies, everything in the cgroup dies, regardless of reparenting or setsid.
Watchdog in the child — crude but portable:
worker() {
local parent=$PPID
while kill -0 "$parent" 2>/dev/null; do sleep 1; done &
local watcher=$!
real_work
kill "$watcher" 2>/dev/null
}Gotchas worth knowing
- •Zombies aren't your problem here. Bash reaps its own children via
wait; orphans get reparented to PID 1 (or the nearestPR_SET_CHILD_SUBREAPER), which reaps them. The problem is orphans that keep running, not zombies. - •
kill 0is shorthand for "my process group" and includes the caller — alwaystrap -first. - •Pipelines spawn subshells.
$!gives you the last element's PID, not the whole pipeline. Process-group killing handles this; PID tracking doesn't. - •Daemonizing children escape everything except cgroups, since they call
setsid()themselves. - •
waitreturns immediately on a signal if you have a trap installed, so loop it:while wait; do :; doneor usewait -nin a loop if you need per-child exit codes.
For most scripts, the setsid re-exec plus kill -- -$$ trap is the right amount of engineering. Reach for systemd-run --scope when correctness actually matters.
Exact recordShowHideThe model id, the run, the verbatim text with line numbers and its full sha256
- Scored
- answered
- Received
- 2026-09-30 09:06:50 UTC
- Run
- 20260930T090007Z-45ce12
- Served by
- Claude Platform on AWS · HTTP 200 · finish stop
- Size
- 3,707 characters · 2,862 tokens out incl. hidden reasoning · 37.9 s
End of response · 110 lines · 3,707 characters · sha256 eaec8c614173fc56b7481c99003efaf25cca647eab05313527f751d691b977a8