Remote plugin cache (~/.claude/remote/plugins) grows unbounded: stale .in_use dead-PID locks starve the GC + non-deterministic materialization

Resolved 💬 1 comment Opened Jun 3, 2026 by leonardsellem Closed Jun 3, 2026

Summary

The remote/cloud agent backend (ccd-cli, invoked from ~/.claude/remote/) accumulates plugin checkout directories in ~/.claude/remote/plugins/ without bound. On one host this reached 955 MB / 87,401 files / 356 directories, growing ~120 dirs/day, for only ~18 unique plugins. Disk wasn't the main pain — it was 42,165 stale lock files (inodes) and an ever-growing cache.

Root cause is two compounding defects in the remote backend's plugin-cache management.

Environment

  • ccd-cli version: 2.1.160 (also saw stale 2.1.149, 2.1.156 binaries left undeleted in ~/.claude/remote/ccd-cli/)
  • Linux, remote/cloud sessions (claude.ai "remote" agent backend). Each session launches ccd-cli with ~18 --plugin-dir flags pointing at content-hash dirs under ~/.claude/remote/plugins/.

Defect 1 — Non-deterministic materialization (same plugin → new hash dir every session)

The per-session plugin dir name is not a stable content hash. Two checkouts of the same plugin had byte-identical trees but different hash-dir names:

# two posthog plugin dirs, excluding .git and .in_use:
content sha 873f9897becf68a2 = e70745a46e826ad0...589be594
content sha 0230ee77c50c26d9 = e70745a46e826ad0...589be594   # identical content, different dir

So every remote session re-materializes a fresh dir for each plugin instead of reusing the existing one. (4 Anthropic-managed plugins — design, seo, brand-voice, operationsdo get a stable reused hash; everything sourced from external git marketplaces churns.) Likely the hash is computed over a freshly-cloned .git/tarball whose bytes vary (embedded timestamps), rather than over the plugin tree.

Defect 2 — The reference-count GC is starved by a stale-lock leak (the real bottleneck)

Each plugin dir has an .in_use/<pid> directory of reference-count lock files. Observed behavior:

  • 0 dirs ever had an empty .in_use/ → the native reclaimer does delete a dir the instant its .in_use/ empties. The GC logic works.
  • 42,165 .in_use/<pid> files belonged to dead PIDs. Remote/cloud sessions are SIGKILLed on exit and never remove their lock files, so .in_use/ stays permanently non-empty and the reclaimer never fires.
  • 206 dirs contained only dead PIDs — they would be reclaimed immediately if the dead locks were cleared.
  • The binary contains the string "Plugin cache cleanup failed", suggesting the startup/exit cleanup that should reap these is erroring out.

Net: Defect 1 would be harmless if dead-session dirs were reclaimed. They aren't, because dead-PID locks (Defect 2) keep every dir looking "in use" forever.

Reproduction / evidence (read-only)

cd ~/.claude/remote/plugins
# unique plugins vs total dirs
for d in */; do grep -m1 '"name"' "$d.claude-plugin/plugin.json" 2>/dev/null; done | sort | uniq -c | sort -rn
# total lock files and how many are dead
find . -path '*/.in_use/*' -type f | wc -l
for f in $(find . -path '*/.in_use/*' -type f); do kill -0 "$(basename "$f")" 2>/dev/null || echo dead; done | wc -l

Impact

  • Unbounded inode + disk growth on long-lived remote hosts (87k files / 955 MB / +120 dirs/day here).
  • Stale ccd-cli version binaries (~230 MB each) are also never pruned.

Suggested fixes

  1. Validate PID liveness before treating .in_use/<pid> as busy — reap dead-PID locks at session start/exit (and/or have the reclaimer kill -0 each PID rather than trusting file presence). This alone bounds the cache.
  2. Make materialization content-deterministic so the same plugin reuses one dir across sessions (hash the plugin tree, not a freshly-cloned .git/tarball).
  3. Prune old ccd-cli version binaries in ~/.claude/remote/ccd-cli/, keeping the active one.
  4. Surface/why the existing "Plugin cache cleanup" path is failing.

Workaround

A safe external GC that reaps dead-PID locks (un-jamming the native reclaimer), then removes orphan dirs not referenced by any running ccd-cli --plugin-dir and holding no alive PID, reclaimed 956 MB → 287 MB and 87k → 17k files with zero impact on running sessions.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗