Claude on Question 14, Sep 28
I'm writing a bash script to purge orphaned children after the parent dies. What's the idiomatic way?The key gotcha first
Once the parent dies, orphans are reparented to PID 1 (or the nearest subreaper), so you can't find them by walking PPIDs after the fact. Any reliable scheme has to establish the kill mechanism before the parent exits. Practically that means one of: process groups, PR_SET_PDEATHSIG, or a cgroup/PID namespace.
1. Trap + direct children (simplest)
#!/usr/bin/env bash
set -euo pipefail
cleanup() {
trap - EXIT INT TERM # prevent re-entry
local pids
pids=$(jobs -p)
[[ -n $pids ]] && kill -- $pids 2>/dev/null
wait 2>/dev/null || true # reap, don't leave zombies
}
trap cleanup EXIT INT TERM
worker_a & worker_b &
waitGood enough when children don't spawn their own children. jobs -p only knows about direct jobs.
2. Process groups (catches grandchildren)
Enable job control in the script so each background job becomes its own process group leader, then kill the whole group with a negative PID:
#!/usr/bin/env bash set -euo pipefail set -m # each bg job → its own pgid ./spawns_subprocesses & pid=$! trap 'kill -TERM -- -"$pid" 2>/dev/null' EXIT INT TERM wait "$pid"
This is my default recommendation. The alternative you'll see a lot is trap 'kill 0' EXIT, but kill 0 signals your own process group — which, if the script was invoked from another script without job control, is the caller's group too. It'll shoot your parent. If you want the whole-group approach, make yourself a group leader explicitly:
if [[ -z ${_REEXEC:-} ]]; then
export _REEXEC=1
exec setsid --wait "$0" "$@" # --wait propagates exit status
fi
trap 'trap - TERM; kill -- -$$' TERM EXITTradeoff: setsid detaches the controlling terminal, so Ctrl-C from the shell no longer reaches you. Use set -m instead if you care about interactive signals.
3. PR_SET_PDEATHSIG — survives SIGKILL of the parent
Traps do not run on SIGKILL, OOM kill, or a hard crash. For a kernel-enforced guarantee:
setpriv --pdeathsig TERM -- ./child.sh &
The kernel signals the child the moment its parent thread exits, no cooperation needed. Caveats: Linux-only (util-linux ≥ 2.33), it fires when the creating thread exits (not necessarily the whole process), it's cleared across a setuid exec, and it only reaches the direct child — combine with process groups for descendants.
4. PID namespace — the blunt instrument
unshare --user --map-root-user --pid --fork --kill-child --mount-proc ./job.sh
Everything inside is a descendant of the namespace's PID 1; when unshare dies, --kill-child tears the namespace down and the kernel reaps the entire tree. Nothing can escape, including deliberately daemonized processes. Similar effect from systemd-run --scope --user, which puts the tree in a cgroup that gets emptied on exit.
Other things that will bite you
- •Always
waitafter killing, or you accumulate zombies until your script exits. - •Reset the trap inside the handler (
trap - EXIT), otherwise a kill that triggersEXITre-enters cleanup. - •
$$in a subshell is still the parent's PID; use$BASHPIDfor the current subshell. - •Children that
setsidthemselves, or are started withnohup/disown, leave your process group and will survive everything except the namespace/cgroup approaches. - •Consider escalation rather than straight
SIGKILL:kill -TERM, sleep briefly, thenkill -KILLon whatever's left, so children can flush state.
If you're stuck with already-orphaned strays from an earlier run, the only real options are matching a recorded PID file (validating with start time via ps -o lstart= to avoid PID reuse) or pkill -f on a distinctive command-line pattern — both fragile, which is why you want the group/pdeathsig setup in place beforehand.
Exact recordShowHideThe model id, the run, the verbatim text with line numbers and its full sha256
- Scored
- answered
- Received
- 2026-09-28 09:06:45 UTC
- Run
- 20260928T090007Z-07d4be
- Served by
- Claude Platform on AWS · HTTP 200 · finish stop
- Size
- 3,856 characters · 3,995 tokens out incl. hidden reasoning · 53.3 s
End of response · 79 lines · 3,856 characters · sha256 1b0b74005bd2ede449419f83cce663585c4ae866857704ce42c1e7a688951cff