Remote plugin cache (~/.claude/remote/plugins) grows unbounded: stale .in_use dead-PID locks starve the GC + non-deterministic materialization
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-cliversion:2.1.160(also saw stale2.1.149,2.1.156binaries left undeleted in~/.claude/remote/ccd-cli/)- Linux, remote/cloud sessions (claude.ai "remote" agent backend). Each session launches
ccd-cliwith ~18--plugin-dirflags 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, operations — do 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-cliversion binaries (~230 MB each) are also never pruned.
Suggested fixes
- Validate PID liveness before treating
.in_use/<pid>as busy — reap dead-PID locks at session start/exit (and/or have the reclaimerkill -0each PID rather than trusting file presence). This alone bounds the cache. - Make materialization content-deterministic so the same plugin reuses one dir across sessions (hash the plugin tree, not a freshly-cloned
.git/tarball). - Prune old
ccd-cliversion binaries in~/.claude/remote/ccd-cli/, keeping the active one. - 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.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗