Claude on Question 14, Sep 29
I'm writing a bash script to purge orphaned children after the parent dies. What's the idiomatic way?The short answer
For direct children, trap EXIT and kill your job list:
#!/usr/bin/env bash
set -euo pipefail
cleanup() {
local pids
pids=$(jobs -rp)
if [[ -n $pids ]]; then
kill $pids 2>/dev/null || true # unquoted on purpose: word-split
wait 2>/dev/null || true
fi
}
trap cleanup EXIT
worker_one &
worker_two &
waitThis is fine for simple cases but has two holes: it doesn't touch grandchildren, and it never runs if your script is SIGKILLed.
Better: kill the whole process group
Grandchildren inherit the process group, so signalling the group catches everything. The trick is making sure you lead your own group, otherwise kill -- -$$ takes out your shell or your CI runner.
#!/usr/bin/env bash
set -euo pipefail
# Re-exec as a session/process-group leader exactly once
if [[ -z ${_IS_LEADER:-} ]]; then
export _IS_LEADER=1
exec setsid --wait "$0" "$@" # --wait propagates the exit status
fi
trap 'trap - TERM EXIT; kill -- -$$' EXIT TERM INTTradeoff: setsid detaches from the controlling terminal, so Ctrl-C no longer reaches the script. If you need to stay interactive, enable job control instead — each background job then becomes its own group leader:
set -m long_running_thing & pgid=$! # with job control, PID == PGID # ... kill -- -"$pgid" # takes the child and all its descendants
Bulletproof: let the kernel do it
Traps can't help you if the parent is SIGKILLed. Two options that survive that:
PR_SET_PDEATHSIG via setpriv (util-linux ≥ 2.33, Linux only):
setpriv --pdeathsig=TERM -- ./child &
Caveats worth knowing: it fires when the parent thread exits (not necessarily the whole process), and it's cleared across a setuid exec.
cgroups, which is what systemd actually uses:
systemd-run --user --scope --collect -- ./myscript.sh
Everything in the scope dies with it, no cooperation from the children required. If you're shipping a unit file, the default KillMode=control-group already does this. Container runtimes give you the same guarantee for free.
If you're hunting already-orphaned processes
When a parent dies, children are reparented to PID 1 (or the nearest ancestor that called PR_SET_CHILD_SUBREAPER). So you can find them:
pgrep -P 1 -f 'my-worker-pattern'
But don't kill by PPID alone — every legitimate daemon on the box has PPID 1. Tag your own processes so you can identify them unambiguously:
# Launch
MY_RUN_ID="$$-$(date +%s)" ./worker &
# Later, find strays from a previous run
for pid in $(pgrep -f './worker'); do
tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null \
| grep -q "^MY_RUN_ID=" && echo "orphan: $pid"
doneA pidfile directory, or matching on a unique argv[0] prefix, works just as well and is less fiddly.
Gotchas
- •
kill 0signals your current process group — convenient, but lethal to your terminal if you haven't established your own group first. - •
waitafterkillmatters; otherwise you exit while children are still shutting down and leave zombies for init to reap. - •Escalate, don't assume
TERMworks:kill -TERM, sleep a couple seconds, thenkill -KILLon whatever's left. - •
trap ... EXITdoesn't inherit into subshells by default; useset -Eplustrap ... ERRif you need that behaviour. - •Double-signalling: reset the trap inside the handler (
trap - TERM EXIT) so aTERMyou send to your own group doesn't re-enter your cleanup.
For most scripts, the setsid + process-group trap is the sweet spot. Reach for cgroups or --pdeathsig when leaked processes would be genuinely expensive.
Exact recordShowHideThe model id, the run, the verbatim text with line numbers and its full sha256
- Scored
- answered
- Received
- 2026-09-29 09:06:32 UTC
- Run
- 20260929T090007Z-334891
- Served by
- Claude Platform on AWS · HTTP 200 · finish stop
- Size
- 3,695 characters · 3,838 tokens out incl. hidden reasoning · 50.0 s
End of response · 104 lines · 3,695 characters · sha256 83813de4c93895e20e60dfd9e88cbda07b4b7ce665b7cbb8f62274b7f412264b