Invariant
The lock exists so overlapping runs for one session cannot clobber each other.
A lock whose owner is dead must be reclaimable promptly — otherwise the
mechanism protecting archives becomes the reason one is lost.
Current violation (observed 2026-08-12)
Lock ownership is established by PID alone:
opid="${other##*.}"
if ! kill -0 "$opid" 2>/dev/null; then
rm -rf "$other" || true # dead owner — name pins identity, safe
elif [[ -n "$(find "$other" -maxdepth 0 -mmin +60)" ]]; then
rm -rf "$other" || true # recycled-PID backstop
If a hook is killed after creating its lock and the OS later reuses that PID for
an unrelated live process, kill -0 succeeds and the stale lock reads as
owned. The -mmin +60 backstop is the only escape, so for up to an hour:
- each SessionEnd for that session waits out its full retry budget
(lock_retries × lock_sleep, default 60 × 1s), then exits without archiving
- the invocation that would have captured the session's final transcript can
be the one that gives up, so that session's archive is lost or left stale
The comment calls this the "recycled-PID backstop", which is accurate — the
gap is that an hour is a long time to be wrong when the affected runs fail
silently (exec 2>/dev/null, trap 'exit 0' ERR).
Rated P2: PID reuse inside an hour requires heavy process churn, and the common
paths (clean exit, killed process whose PID is not reused) already work.
Verify
grep -n 'kill -0' templates/claude-hooks/session-end-archive/session-end-archive.sh
grep -n 'mmin +60' templates/claude-hooks/session-end-archive/session-end-archive.sh
Ownership resting on kill -0 with only an age backstop means the window is
live. Resolved when the owner's identity is validated beyond PID.
Options
- Record process start time alongside the PID — the lock name already pins
the PID, so .lock-<sid>.<pid>.<starttime> makes reuse detectable: a
recycled PID has a different start time. Reading it portably differs across
Linux (/proc/<pid>/stat field 22) and macOS (ps -o lstart=), which is
the main cost.
- Write an owner token into the lock — e.g. a UUID plus the PID, with the
owner re-verifying its own token before proceeding. Portable, but adds a
second file to the protocol.
- Shorten the backstop — cheapest, and it narrows the window without
closing it; a bad trade alone, reasonable alongside 1 or 2.
Acceptance criteria
Found while wiring this hook into harmon-init's devcontainer
(evanharmon1/harmon-init#816).
Invariant
The lock exists so overlapping runs for one session cannot clobber each other.
A lock whose owner is dead must be reclaimable promptly — otherwise the
mechanism protecting archives becomes the reason one is lost.
Current violation (observed 2026-08-12)
Lock ownership is established by PID alone:
If a hook is killed after creating its lock and the OS later reuses that PID for
an unrelated live process,
kill -0succeeds and the stale lock reads asowned. The
-mmin +60backstop is the only escape, so for up to an hour:(
lock_retries×lock_sleep, default 60 × 1s), then exits without archivingbe the one that gives up, so that session's archive is lost or left stale
The comment calls this the "recycled-PID backstop", which is accurate — the
gap is that an hour is a long time to be wrong when the affected runs fail
silently (
exec 2>/dev/null,trap 'exit 0' ERR).Rated P2: PID reuse inside an hour requires heavy process churn, and the common
paths (clean exit, killed process whose PID is not reused) already work.
Verify
Ownership resting on
kill -0with only an age backstop means the window islive. Resolved when the owner's identity is validated beyond PID.
Options
the PID, so
.lock-<sid>.<pid>.<starttime>makes reuse detectable: arecycled PID has a different start time. Reading it portably differs across
Linux (
/proc/<pid>/statfield 22) and macOS (ps -o lstart=), which isthe main cost.
owner re-verifying its own token before proceeding. Portable, but adds a
second file to the protocol.
closing it; a bad trade alone, reasonable alongside 1 or 2.
Acceptance criteria
Found while wiring this hook into harmon-init's devcontainer
(evanharmon1/harmon-init#816).