MODELDRIFT.WATCH

Question 14 · Sep 26 · Day 17

Claude on Question 14, Sep 26

I'm writing a bash script to purge orphaned children after the parent dies. What's the idiomatic way?

Short answer

Two mechanisms, and you usually want both:

  • 1.trap + kill the process group — handles the normal case (script exits, is Ctrl-C'd, or gets SIGTERM).
  • 2.PR_SET_PDEATHSIG via setpriv --pdeathsig — handles the case a trap can't: the parent getting SIGKILL'd or segfaulting, where no cleanup code runs at all.

Pattern 1: trap + process group

#!/usr/bin/env bash
set -euo pipefail

cleanup() {
  trap '' TERM        # don't kill ourselves mid-cleanup
  kill -TERM -- -$$ 2>/dev/null || true
  sleep 0.2
  kill -KILL -- -$$ 2>/dev/null || true
}
trap cleanup EXIT INT TERM HUP

sleep 300 &
sleep 300 &
wait

The negative PID means "whole process group," so this reaps grandchildren too — which a saved-PID list won't.

Caveat: -$$ only works if your script actually is the process group leader. That's true when launched as a job from an interactive shell, but not when invoked from another script (job control off → you inherit the caller's group). Then kill -- -$$ either errors out or, worse, nukes your caller's siblings.

Pattern 2: guarantee your own process group

Re-exec into a fresh session so -$$ is always meaningful:

if [[ -z ${_IN_NEW_SESSION:-} ]]; then
  export _IN_NEW_SESSION=1
  exec setsid --wait "$0" "$@"
fi

setsid --wait propagates the exit status. Trade-off: you detach from the controlling terminal, so Ctrl-C from the shell no longer reaches you — you need to forward signals yourself if that matters.

Pattern 3: PDEATHSIG (the SIGKILL-proof one)

setpriv --pdeathsig=TERM -- long_running_thing &

The kernel signals the child the moment its parent dies, regardless of how. Requires util-linux ≥ 2.33 and is Linux-only.

Two gotchas worth knowing: the signal fires when the parent thread exits (not just the process), and --pdeathsig is cleared across setuid execs. It also only covers direct children — grandchildren need their own setting.

Pattern 4: tracked PIDs, when you need precision

pids=()
spawn() { "$@" & pids+=("$!"); }

cleanup() {
  for p in "${pids[@]}"; do
    kill -TERM "$p" 2>/dev/null || true
  done
  wait
}
trap cleanup EXIT

spawn sleep 300
spawn sleep 300
wait -n   # returns when the first one finishes

Use this when children need to die in a specific order or get different signals. It won't catch grandchildren.

Other notes

  • •timeout --foreground is the right flag when timeout is inside a script that's not in the foreground process group; without it the signal may not reach your command.
  • •Orphans reparent to PID 1, not to your grandparent — so there's no way to walk back up and find them after the fact. Track them before you lose the chance.
  • •cgroups are the bulletproof option. systemd-run --scope --user ./myscript puts everything in a cgroup that gets torn down as a unit, and nothing can escape by double-forking. This is what container runtimes do.
  • •A child can also self-police: while kill -0 "$PPID" 2>/dev/null; do ...; done, or poll for $PPID becoming 1. Racy if PIDs get recycled, but fine for long-lived loops.

For most scripts, Pattern 1 plus Pattern 2's re-exec guard is the sweet spot. Add --pdeathsig on the children you really can't afford to leak.

sha256 2f9d2ae37dc2… · 40 tokens in · 2,974 out · 39.6 s · via Claude Platform on AWS · stopped on its own

Sep 26, the whole dayQuestion 14, every day

Exact recordShowHideThe model id, the run, the verbatim text with line numbers and its full sha256
Model
anthropic/claude-opus-5
pinned anthropic/claude-opus-5-20260723; alias resolved to anthropic/claude-opus-5-20260723 at 2026-09-26 09:00:06 UTC (matches the pin)
Date
2026-09-26 · run 20260926T090007Z-89c0b7 · scorer v1
Scored
answered
sha256
2f9d2ae37dc2c798159936bcaf05e2cba86e7b7f40c80c171ee8f4978845a82b
Permalink
https://modeldrift.watch/r/2026-09-26/R5/anthropic/claude-opus-5/
Scored
answered
Received
2026-09-26 09:06:25 UTC
Run
20260926T090007Z-89c0b7
Served by
Claude Platform on AWS · HTTP 200 · finish stop
Size
3,279 characters · 2,974 tokens out incl. hidden reasoning · 39.6 s
## Short answer
Two mechanisms, and you usually want both:
1. **`trap` + kill the process group** — handles the normal case (script exits, is Ctrl-C'd, or gets SIGTERM).
2. **`PR_SET_PDEATHSIG`** via `setpriv --pdeathsig` — handles the case a trap *can't*: the parent getting SIGKILL'd or segfaulting, where no cleanup code runs at all.
## Pattern 1: trap + process group
```bash
#!/usr/bin/env bash
set -euo pipefail
cleanup() {
trap '' TERM # don't kill ourselves mid-cleanup
kill -TERM -- -$$ 2>/dev/null || true
sleep 0.2
kill -KILL -- -$$ 2>/dev/null || true
}
trap cleanup EXIT INT TERM HUP
sleep 300 &
sleep 300 &
wait
```
The negative PID means "whole process group," so this reaps grandchildren too — which a saved-PID list won't.
**Caveat:** `-$$` only works if your script actually *is* the process group leader. That's true when launched as a job from an interactive shell, but **not** when invoked from another script (job control off → you inherit the caller's group). Then `kill -- -$$` either errors out or, worse, nukes your caller's siblings.
## Pattern 2: guarantee your own process group
Re-exec into a fresh session so `-$$` is always meaningful:
```bash
if [[ -z ${_IN_NEW_SESSION:-} ]]; then
export _IN_NEW_SESSION=1
exec setsid --wait "$0" "$@"
fi
```
`setsid --wait` propagates the exit status. Trade-off: you detach from the controlling terminal, so Ctrl-C from the shell no longer reaches you — you need to forward signals yourself if that matters.
## Pattern 3: PDEATHSIG (the SIGKILL-proof one)
```bash
setpriv --pdeathsig=TERM -- long_running_thing &
```
The kernel signals the child the moment its parent dies, regardless of *how*. Requires util-linux ≥ 2.33 and is Linux-only.
Two gotchas worth knowing: the signal fires when the **parent thread** exits (not just the process), and `--pdeathsig` is cleared across `setuid` execs. It also only covers direct children — grandchildren need their own setting.
## Pattern 4: tracked PIDs, when you need precision
```bash
pids=()
spawn() { "$@" & pids+=("$!"); }
cleanup() {
for p in "${pids[@]}"; do
kill -TERM "$p" 2>/dev/null || true
done
wait
}
trap cleanup EXIT
spawn sleep 300
spawn sleep 300
wait -n # returns when the first one finishes
```
Use this when children need to die in a specific order or get different signals. It won't catch grandchildren.
## Other notes
- **`timeout --foreground`** is the right flag when `timeout` is inside a script that's not in the foreground process group; without it the signal may not reach your command.
- **Orphans reparent to PID 1**, not to your grandparent — so there's no way to walk back up and find them after the fact. Track them before you lose the chance.
- **cgroups are the bulletproof option.** `systemd-run --scope --user ./myscript` puts everything in a cgroup that gets torn down as a unit, and nothing can escape by double-forking. This is what container runtimes do.
- A child can also self-police: `while kill -0 "$PPID" 2>/dev/null; do ...; done`, or poll for `$PPID` becoming 1. Racy if PIDs get recycled, but fine for long-lived loops.
For most scripts, Pattern 1 plus Pattern 2's re-exec guard is the sweet spot. Add `--pdeathsig` on the children you really can't afford to leak.

End of response · 82 lines · 3,279 characters · sha256 2f9d2ae37dc2c798159936bcaf05e2cba86e7b7f40c80c171ee8f4978845a82b