Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion .claude/hooks/guard-process-kill.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,35 @@
# scripts/gen-sdui-manifest.sh
# pkill -P "$pid" / pkill -s "$sid" — the same selectors on pkill, with NO pattern
# operand. Add a pattern and it is blocked again.
# pgrep -f foo / ps aux | grep node — a READ. Nothing dies; look all you like.
# pgrep -f foo / ps aux | grep node — a READ. Nothing dies; look all you like. But a
# READ is not a WAIT — see the section below.
#
# ## The WAIT side of the same table: a READ is safe, a WAIT on what it matched is not
#
# WAIT ONLY ON A PID YOU RECORDED.
#
# The line above is right about SIGNALS, and that is exactly where the trap sits.
# Name-matching a container-wide process table does not stop being hazardous when nothing is
# killed: a name-matched WAIT has the same root cause (one process table, text matching, no
# ownership check) and the same property that makes the kill rule a RULE rather than advice
# — it reports SUCCESS.
#
# `pgrep -f "$PAT"` run from a shell whose OWN command line contains "$PAT" matches the
# asker itself; `head -1` takes it, the `echo` prints a plausible PID, and `tail --pid=$BP`
# then waits for its own parent while that parent waits for it. Measured on this repo's
# container: three such waiters sat deadlocked for five hours, on cards whose agent had been
# dead for four — the real work had long since exited and no lock was held, so the wait
# was waiting for nothing and said nothing about it.
#
# Record the pid the same way the kill rule prescribes: `cmd & pid=$!` then `wait "$pid"`, a
# pidfile, or a wrapper that prints its own PID. Ordering cannot repair a bare `pgrep -f
# "$PAT" | head -1` whose pattern is live in the asker's cmdline — a discovery by pattern
# must exclude the asker and its ancestors, or it is not a discovery.
#
# ⛔ Deliberately NOT guarded, and the asymmetry is the reason: a name-matched KILL
# destroys a NEIGHBOUR's run, invisibly to whoever fired it, which is what buys a hook. A
# wait that never returns spends only the ASKER's own budget. So this half is a written
# rule, not a refusal.
#
# Deliberate exception (you know the match can only be yours): OS_ALLOW_PROCESS_KILL=1.
#
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -530,8 +530,8 @@ Even inside your own worktree, operate defensively:
A running dev server you didn't start probably belongs to another agent or the user;
killing it (or its port) breaks their in-flight work. Spin up your own instance on a
random high port (`pnpm dev -- --fresh -p <random>`) and **shut it down yourself when
the task is done** (`kill $(lsof -ti tcp:<port>)`). Don't leave orphan servers behind.
⛔ One process table per container: **kill only a PID you recorded, never a name** (`guard-process-kill.sh`).
the task is done** (`kill $(lsof -ti tcp:<port>)`). ⛔ One process table per container: **kill only a PID you
recorded, and wait only on one, never a name** — `pgrep -f` matches the asking shell (`guard-process-kill.sh`).
9. **After pulling `main` into a long-lived worktree, refresh its build state before you
trust a single test or gate.** A worktree open across several merges accumulates
artefacts stale relative to the source, and every one of them fails **as if your change
Expand Down
Loading