Skip to content

session-end-archive: a recycled PID keeps a dead lock alive for up to an hour, silently skipping archives #441

Description

@evanharmon1

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

  1. 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.
  2. 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.
  3. Shorten the backstop — cheapest, and it narrows the window without
    closing it; a bad trade alone, reasonable alongside 1 or 2.

Acceptance criteria

  • A lock whose PID was recycled is detected as stale without waiting for the age backstop
  • The detection is portable across Linux and macOS
  • A live lock held by a genuine contender is still respected
  • The existing offline contention tests cover the recycled-PID case
  • No new failure path makes session exit noisy or blocking

Found while wiring this hook into harmon-init's devcontainer
(evanharmon1/harmon-init#816).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions