Claude on Question 14, Sep 12
I'm writing a bash script to purge orphaned children after the parent dies. What's the idiomatic way?Short answer
Trap EXIT and kill the whole process group, not individual PIDs. jobs -p only knows about your direct background jobs, so grandchildren leak.
#!/usr/bin/env bash
set -euo pipefail
cleanup() {
trap - EXIT # don't re-enter
trap '' TERM INT # don't kill ourselves with the group signal
local pgid
pgid=$(ps -o pgid= -p "$$" | tr -d '[:space:]')
if [[ "$pgid" == "$$" ]]; then
kill -TERM -- "-$pgid" 2>/dev/null || true
sleep 0.2
kill -KILL -- "-$pgid" 2>/dev/null || true
else
kill -TERM $(jobs -p) 2>/dev/null || true # fallback: not group leader
fi
}
trap cleanup EXIT INT TERMTwo details that bite people:
- •The
--before-$pgidis required, otherwisekillparses the negative number as an option. - •
kill -- -$$(the one-liner you'll see everywhere) is only correct when$$is the group leader. That's true when a script is launched from an interactive shell with job control, but not when it's invoked from another script — there it inherits the parent's PGID, and you'll either no-op or nuke your caller's group. Hence the check above.
If you want a guarantee
Traps don't run on SIGKILL, so no pure-bash solution survives kill -9 of the parent. On Linux you have two escalations:
Per-child, kernel-enforced — PR_SET_PDEATHSIG via setpriv:
setpriv --pdeathsig=TERM -- long_running_task &
The kernel signals the child when its parent dies, including on SIGKILL. Caveats: it's cleared across a setuid exec, it fires when the forking thread exits (not necessarily the whole parent), and it isn't inherited — grandchildren need their own.
Whole-tree, cgroup-enforced — the actually-airtight option:
systemd-run --user --scope --collect -- ./my_script.sh
Everything lands in one cgroup; when the scope goes away the kernel reaps the entire subtree regardless of reparenting. This is what you want if the processes are expensive or hold locks.
Guaranteeing group leadership
If you'd rather not do the PGID check, force a fresh group at startup:
if [[ -z ${_REEXEC:-} ]]; then
export _REEXEC=1
exec setsid --wait "$0" "$@"
fi
trap 'kill -- -$$' EXIT # now safe: we're definitely the leaderTradeoff: setsid detaches from the controlling terminal, so Ctrl-C no longer reaches the script. You'd need to forward signals from a wrapper, which somewhat defeats the simplicity.
Graceful shutdown
If children need to flush state, wait between TERM and KILL instead of a blind sleep:
kill -TERM -- "-$pgid" 2>/dev/null || true
for _ in {1..50}; do
kill -0 -- "-$pgid" 2>/dev/null || break
sleep 0.1
done
kill -KILL -- "-$pgid" 2>/dev/null || trueFor most scripts the process-group trap is plenty. Reach for cgroups the moment correctness actually matters.
Exact recordShowHideThe model id, the run, the verbatim text with line numbers and its full sha256
- Scored
- answered
- Received
- 2026-09-12 09:06:46 UTC
- Run
- 20260912T090021Z-c0c86e
- Served by
- Claude Platform on AWS · HTTP 200 · finish stop
- Size
- 2,865 characters · 3,067 tokens out incl. hidden reasoning · 40.7 s
End of response · 77 lines · 2,865 characters · sha256 3d0ea9f9286e0628c7b9ad5d84aa3ee3717bf1f0a9467cd2532e7bd632bd6353