Claude on Question 14, Sep 19
I'm writing a bash script to purge orphaned children after the parent dies. What's the idiomatic way?For a well-behaved exit, the idiom is a trap plus killing the child process group — not individual PIDs, since grandchildren are what actually leak. For the "parent got SIGKILLed" case, traps can't help you at all and you need kernel-level help (--pdeathsig, PID namespaces, or cgroups).
The common case: trap + process group
The key detail people miss: a non-interactive bash script is not a process-group leader, it inherits its caller's group. So kill -- -$$ can nuke your terminal or CI runner. Turn on job control so each background job gets its own group:
#!/usr/bin/env bash
set -euo pipefail
set -m # job control: each bg job becomes a pgroup leader
children=()
spawn() {
"$@" &
children+=("$!") # with set -m, $! is also the PGID
}
cleanup() {
trap - EXIT INT TERM # don't re-enter
local pgid
for pgid in "${children[@]}"; do
kill -TERM "-$pgid" 2>/dev/null || true
done
# give them a moment, then escalate
sleep 0.5
for pgid in "${children[@]}"; do
kill -KILL "-$pgid" 2>/dev/null || true
done
wait 2>/dev/null || true # reap
}
trap cleanup EXIT INT TERM
spawn ./some-server
spawn ./some-worker
waitThe - prefix on the PID is what makes kill target the whole group. Recording the PGID up front matters because once the parent dies the orphans are reparented to init (or a subreaper), so pkill -P $$ won't find them anymore.
If you only ever have direct children and don't care about trees, the one-liner version is fine:
trap 'kill $(jobs -p) 2>/dev/null' EXIT
When the parent is SIGKILLed
No trap runs, so you need the kernel to do the cleanup. Three options, roughly in order of how invasive they are:
setpriv --pdeathsig (util-linux ≥ 2.33) — wraps prctl(PR_SET_PDEATHSIG). Each child gets signalled when its parent dies:
setpriv --pdeathsig=TERM -- ./some-server &
Caveat: it fires when the direct parent's thread exits, so it doesn't propagate down a tree on its own, and it's lost across an intermediate process that doesn't re-arm it.
PID namespace — unshare --pid --fork --kill-child makes your script PID 1 of a new namespace; when it dies, the kernel kills everything inside:
unshare --pid --fork --mount-proc --kill-child -- ./my-script.sh
Needs privileges or CONFIG_USER_NS / unprivileged userns enabled.
cgroup/scope — most robust on systemd hosts, and what service managers actually use:
systemd-run --user --scope --collect -- ./my-script.sh
Everything lands in one cgroup that gets torn down as a unit.
Two other notes
- •If you want to reap orphans rather than kill them (e.g. you're a supervisor and want to keep collecting exit statuses), that's
PR_SET_CHILD_SUBREAPER, not available from bash directly — use a real init liketiniordumb-initas PID 1. - •
shopt -s huponexitonly applies to interactive login shells; it won't do anything in a script.
For most scripts, the set -m + trap version above plus setpriv --pdeathsig on long-running children covers both graceful and abrupt death without needing privileges.
Exact recordShowHideThe model id, the run, the verbatim text with line numbers and its full sha256
- Scored
- answered
- Received
- 2026-09-19 09:06:40 UTC
- Run
- 20260919T090005Z-86161c
- Served by
- Claude Platform on AWS · HTTP 200 · finish stop
- Size
- 3,160 characters · 3,037 tokens out incl. hidden reasoning · 42.8 s
End of response · 80 lines · 3,160 characters · sha256 460310859d49eaafd3601dd54ec4b414cb9befe9a0834823cf1776f43a97a017