Plugin cache: stale .in_use PID markers in orphaned version dirs cause intermittent resolution to outdated plugin content
Plugin cache: stale .in_use PID markers left in orphaned version directories cause intermittent resolution to outdated plugin content
Summary
When a plugin is updated to a new version, Claude Code marks the old version directory under ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/ with an .orphaned_at timestamp file. Each version directory also has an .in_use directory containing one marker file per Claude Code process (named by PID, containing {"pid":..., "procStart":...}) that has read from that version.
The bug: a process's .in_use marker is never removed from an old version directory once that process starts reading a newer version instead. Markers accumulate indefinitely across every version a long-lived process has ever touched, rather than being migrated or cleared. We believe this stale .in_use occupancy is why some tool dispatches (Skill()/Agent() calls) intermittently resolve to an old, orphaned plugin version instead of the current one — confirmed independently on two separate machines.
Evidence
On two different developers' machines (independently, not shared state), the same pattern appeared: a single process's PID had .in_use marker files in multiple different version directories for the same plugin, including versions marked .orphaned_at days earlier.
Example from one machine (support-workstation plugin, current version 0.8.13):
0.5.2/.in_use/55971 → {"pid":55971,"procStart":"Fri Sep 18 15:04:39 2026"}
0.8.13/.in_use/55971 → {"pid":55971,"procStart":"Fri Sep 18 15:04:39 2026"}
Same PID, same procStart (confirming it's the same process instance), present in both an .orphaned_at-marked version and the current one. This pattern repeated across multiple PID/version pairs on the same machine, and independently on a second machine with a different set of stale versions (0.7.0/0.3.4 in that case).
On a real, live casework session (transcript available on request — contains customer data, not attached here), we confirmed via direct string search over the session's raw JSONL transcript that 92 references resolved to the current plugin version path, but 28 references resolved to a version 9 releases old (0.3.4) in the same session — including core reference/config files (config/taxonomy.yaml, reference/salesforce.md, teams/channel.md), not just one skill.
Impact
This is not just a performance/caching curiosity — it changes actual runtime behavior non-deterministically within a single session. In the case we traced, a Salesforce write was rejected with INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST for a value that ~40 other live records already carried successfully — plausibly explained by the config file for that dispatch having silently resolved to a 9-version-old snapshot rather than the current one, rather than being a genuine data/picklist issue.
Suspected root cause
The .in_use marker appears to be written whenever a process reads from a plugin version (plausibly during automatic plugin/agent/skill discovery, not only on an explicit invocation), but nothing removes a process's marker from a version once that process has moved on to a newer one. If any part of version resolution or cache eviction treats "does this orphaned directory have any .in_use entries" as a signal that it's still needed, this stale residue would explain both:
- Orphaned directories never getting fully evicted even when nothing is really using them.
- Occasional resolution back to an orphaned version instead of the current one.
Suggested fix direction
- When a process transitions to reading a newer version of a plugin, remove (or migrate) its own
.in_usemarker from any older version directories for that same plugin. - Version resolution for a
Skill()/Agent()dispatch should resolve strictly from whatever the plugin manifest/lockfile declares as current, never influenced by.in_useoccupancy of an alternate version directory.
Workaround (used ourselves in the meantime)
Manually removing every version directory with an .orphaned_at file present (keeping only the version with no .orphaned_at marker) clears the stale state. This is safe as a one-time manual cleanup but isn't a fix — the same accumulation will recur as the plugin is updated further, since the underlying marker-cleanup logic is what's missing.
Environment
- Claude Code version: 2.1.275 (macOS)
- Reproduced independently on two machines
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗