Plugin manager: `.in_use/<pid>` lock files leak when sessions crash

Resolved 💬 3 comments Opened May 14, 2026 by ianplaydon Closed May 17, 2026

Plugin manager: .in_use/<pid> lock files leak when sessions crash

Summary

The plugin manager records active sessions holding a plugin version by writing empty files named after the session's PID into ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/.in_use/. When a Claude Code process exits ungracefully (crash, force-kill, OS reboot before clean shutdown), the corresponding .in_use/<pid> file is not removed. The plugin manager subsequently believes the orphaned version is still in use, so it cannot safely garbage-collect it.

Reproduction

Live evidence collected on 2026-05-13 from one machine that had been running Claude Code over a multi-week period:

$ ls ~/.claude/plugins/cache/community-access/accessibility-agents/3.2.0/.in_use/
18792  2093  2320  23515  29905  36512  57707  63327

$ for pid in 18792 2093 2320 23515 29905 36512 57707 63327; do
    if kill -0 "$pid" 2>/dev/null; then
      echo "PID $pid: alive — $(ps -p $pid -o comm= 2>/dev/null)"
    else
      echo "PID $pid: STALE"
    fi
  done
PID 18792: alive — claude
PID 2093:  alive — claude
PID 2320:  STALE
PID 23515: alive — claude
PID 29905: STALE
PID 36512: STALE
PID 57707: STALE
PID 63327: STALE

4 of 8 lock files (50%) were stale — referencing processes that had long since exited. This blocked the plugin manager from orphan-cleaning the v3.2.0 directory even after v4.0.0 was installed and orphan-flagged.

Proposed fix — two options

Option A: kernel-level advisory locks (flock(2))

Replace the .in_use/<pid> file convention with flock() against a single file per version. The kernel releases the lock on process exit regardless of how the process died.

  • Pros: correct by construction. No stale state possible.
  • Cons: requires a syscall path on Windows (or a Win32 LockFileEx equivalent). Slightly more code than the current file-touch convention.

Option B: stale-PID sweep on every cache read

Keep the file convention but add a sweep step before reading .in_use/:

for pid_file in (cache_dir / ".in_use").iterdir():
    try:
        pid = int(pid_file.name)
    except ValueError:
        pid_file.unlink(missing_ok=True)  # malformed
        continue
    if not _pid_alive(pid):
        pid_file.unlink(missing_ok=True)

_pid_alive() uses kill -0 <pid> on POSIX, OpenProcess on Windows. Cost: a few syscalls per cache operation. Risk: a freshly-spawned PID could collide with a recycled-PID stale entry — rare in practice (PID space is large) but worth noting.

  • Pros: minimal change. Drop-in replacement for the current convention.
  • Cons: still leaves a small window for stale entries between sweeps.

Recommendation

Ship Option B in the short term (low risk, immediate improvement). Plan Option A as a long-term cleanup once the cross-platform implementation cost is budgeted.

Acceptance

  • After force-killing a Claude Code process holding a plugin version, the next plugin-manager interaction (install/update/list) removes the stale .in_use/<pid> file for the dead PID.
  • A pid file referencing a PID that has been recycled to a non-Claude-Code process is treated as stale (the file is removed). Acceptable trade-off given the rarity.

See also

  • Companion issue: .orphaned_at flag has no garbage collector — #58880
  • Companion issue: plugin major-version updates that change agent count drastically should expose a post-update migration hook — #58882

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗