Claude on Question 14, Sep 23
I'm writing a bash script to purge orphaned children after the parent dies. What's the idiomatic way?Standard process-management stuff — the key thing to understand up front is why you have to be deliberate about this: when a parent dies, its children are reparented to PID 1 (or the nearest subreaper), so they keep running happily with no knowledge that anyone wanted them gone. Nothing cleans them up for you.
The common case: trap + kill the process group
If the parent exits normally or catches a signal, kill the whole process group:
#!/usr/bin/env bash
set -euo pipefail
# Make this script its own process group leader so -$$ is meaningful
if [[ -z ${__REEXEC:-} ]]; then
export __REEXEC=1
exec setsid --wait "$0" "$@"
fi
cleanup() {
trap - EXIT TERM INT # don't re-enter
kill -TERM -- "-$$" 2>/dev/null || true
sleep 0.5
kill -KILL -- "-$$" 2>/dev/null || true
}
trap cleanup EXIT TERM INT
# ... spawn work here; grandchildren are caught too
sleep 300 &
waitThe setsid --wait re-exec matters. A script launched from another script inherits its caller's PGID, so kill -- -$$ would be a no-op (or kill 0 would nuke your caller). Interactive shells with job control give the script its own group, which is why the naive version appears to work in testing and then bites you in CI.
The tradeoff: setsid detaches from the controlling terminal, so you lose Ctrl-C delivery. If you need that, drop the re-exec and instead use set -m so each background job becomes its own process group, then kill groups individually:
set -m myworker & worker_pgid=$! # ... kill -TERM -- "-$worker_pgid"
If the parent might be SIGKILLed
Traps don't run on SIGKILL, OOM kill, or a hard crash. For that you need the kernel to notify the child. prctl(PR_SET_PDEATHSIG) is the mechanism, exposed by util-linux ≥ 2.33:
setpriv --pdeathsig TERM -- long_running_thing &
Each child gets SIGTERM the instant its parent thread exits. Two caveats worth knowing: it fires when the parent thread exits (not the process), and it's cleared across setuid transitions. It also only covers direct children — grandchildren need their own --pdeathsig, so combine it with the process-group approach rather than relying on it alone.
The actually-airtight option: cgroups
If the whole tree must die no matter what, put it in a cgroup and kill the cgroup. That's what systemd does:
systemd-run --user --scope --collect -- ./my_script.sh
Kill the scope and every descendant goes with it, regardless of double-forking, reparenting, or setsid tricks. On cgroup v2 you can also write 1 to cgroup.kill. This is the only approach a determined process can't escape.
A note on terminology
If what you actually have is zombies rather than orphans — children that exited but whose status was never collected — that's a different fix entirely. Zombies hold nothing but a PID table entry and can't be killed; you reap them:
wait # all children wait -n # next one to finish wait "$pid" # get its exit status
Orphans need killing; zombies need waiting. Worth being sure which one you're seeing in ps before reaching for kill.
Exact recordShowHideThe model id, the run, the verbatim text with line numbers and its full sha256
- Scored
- answered
- Received
- 2026-09-23 09:07:36 UTC
- Run
- 20260923T090022Z-9b7b3a
- Served by
- Claude Platform on AWS · HTTP 200 · finish stop
- Size
- 3,129 characters · 3,009 tokens out incl. hidden reasoning · 43.0 s
End of response · 72 lines · 3,129 characters · sha256 9cd7ef9256ecbc38851dd74776ba47dbb38c752b4e73a5a0778984c53742a063