Claude on Question 14, Sep 15
I'm writing a bash script to purge orphaned children after the parent dies. What's the idiomatic way?The common case: trap + process-group kill
If your script spawns background jobs and you want them gone when the script exits:
#!/usr/bin/env bash
set -euo pipefail
cleanup() {
trap - EXIT INT TERM # don't re-enter
kill -- -$$ 2>/dev/null # signal the whole process group
wait 2>/dev/null || true
}
trap cleanup EXIT INT TERM
sleep 300 &
some_daemon &
waitTwo gotchas with kill -- -$$ (equivalently kill 0):
- 1.It only works if your script is a process-group leader. When you launch a script from an interactive shell, job control gives it its own group, so you're fine. When it's invoked from another script, from cron, or from a CI runner, it inherits the caller's group — and
kill 0will take out your parent too. Guard against that by making yourself a leader up front:
# re-exec in a new session if we're not already the group leader [[ $$ == $(ps -o pgid= -p $$ | tr -d ' ') ]] || exec setsid "$0" "$@"
setsid also detaches from the controlling terminal, which you may not want; set -m (enable job control) is the lighter-weight alternative, but then each background job gets its own group and you must kill them individually with kill -- -$pid.
- 2.Trap on EXIT doesn't cover every path. Bash won't run an EXIT trap for an untrapped fatal signal, and nothing runs on
SIGKILL. Which leads to the harder version of your question.
If the parent can die without warning
Traps are useless against SIGKILL, OOM kills, or a segfaulting parent. Orphans just get reparented to PID 1 (or the nearest subreaper) and keep running. You need the kernel's help.
PR_SET_PDEATHSIG — Linux asks the kernel to signal the child when its parent dies. Exposed via util-linux 2.33+:
setpriv --pdeathsig TERM -- some_daemon &
Caveats: it triggers on the death of the parent thread, not the process, and it's cleared across a setuid exec. Good enough for most scripts.
Pipe-EOF watchdog — portable, no special tools. The parent holds the write end of a pipe; the child blocks reading it. When the parent dies for any reason, the FD closes and read returns EOF:
# parent
coproc WATCH { read -r _ || true; } # placeholder; see belowMore practically, in the child wrapper:
guard() {
# fd 3 is inherited from the parent and never written to
( read -r -u 3 _ ; kill -TERM 0 ) &
exec "$@"
}
exec 3>&1 # or a real pipe via mkfifo / process substitutionPolling $PPID — crude but works everywhere, and is what a lot of shell supervisors actually do:
while kill -0 "$PPID" 2>/dev/null; do sleep 1; done kill -TERM 0
Note that after the parent dies, $PPID in bash doesn't update (it's cached at startup), so kill -0 $PPID correctly starts failing — that's the signal you want. On Linux you can read /proc/self/stat field 4 for the live value.
The robust answer: cgroups
If you're on systemd and want a guarantee rather than a convention, put the whole tree in a scope and let the kernel account for it:
systemd-run --user --scope --unit=myjob-$$ ./my-script.sh # teardown, at any time, from anywhere: systemctl --user stop myjob-$$.scope
Nothing escapes a cgroup — not double-forking daemons, not setsid, not processes that ignore SIGTERM (the scope escalates to SIGKILL). This is the only approach that's actually airtight.
Related but distinct: zombies
Worth separating, since the terminology overlaps. A zombie is an already-dead child whose exit status hasn't been collected. In bash, wait reaps them, and wait -n reaps whichever finishes first. Zombies in a script are almost always a sign you forgot a wait; they can't be killed, only reaped. Orphans are the opposite problem — very much alive, just re-parented.
Exact recordShowHideThe model id, the run, the verbatim text with line numbers and its full sha256
- Scored
- answered
- Received
- 2026-09-15 09:07:03 UTC
- Run
- 20260915T090004Z-599f39
- Served by
- Claude Platform on AWS · HTTP 200 · finish stop
- Size
- 3,828 characters · 3,490 tokens out incl. hidden reasoning · 49.2 s
End of response · 87 lines · 3,828 characters · sha256 697fc2485f146ba27191db304db9462558cdc162a5536f42d426aeb17d391fb5