Scoped skills become uninvocable ("Unknown skill") after compaction — dynamicSkillDirs/sentSkillNames not reset
TL;DR
Scoped skills defined in a nested .claude/skills/ below cwd are discovered lazily, the first time a file under that subtree is read. After a context compaction, any such skill becomes permanently uninvocable: Skill() returns Unknown skill, and reading more files under that same subtree does not bring it back. Recovery only happens if some unrelated filesystem event happens to re-trigger the skill-directory watcher.
This is not the "skills vanish after compaction" behavior working as intended. The code deliberately skips re-injecting the full skill listing on compaction (sentSkillNames is intentionally not reset) to save about 4K tokens, and that tradeoff is reasonable and not what this issue is about. The actual bug is that dynamicSkillDirs, the set marking which lazy-discovery boundaries have already fired, is also never reset on compaction. That means re-discovery can never fire again for a skill that was discovered but not yet invoked at the moment compaction happened, and the fix costs nothing: clearing dynamicSkillDirs on compaction is token-neutral (unlike re-injecting the listing) and simply re-arms the normal lazy re-discovery path.
How this differs from #74990
Related, but a distinct and more severe failure mode — a fix for #74990 alone would not resolve it:
- #74990: the Available-skills system-reminder (the listing) is dropped post-compaction, but the report states the registry is intact and
/reload-skillsrecovers with "(no changes)" — skills remain invocable, the model just loses awareness of the list. - This issue: the scoped skill is genuinely not invocable —
Skill("<name>")errors withUnknown skill. For scoped skills, registry membership is tied to lazy discovery, and that discovery state is also frozen across compaction.
They likely share one root cause (the listing not being re-injected) but this issue has a second root cause specific to scoped skills.
Root cause
Two module-level dedup sets gate skill re-surfacing, and neither is reset on compaction:
sentSkillNames(utils/attachments.ts) — gates theskill_listingcatalog.services/compact/compact.tsandservices/compact/postCompactCleanup.tsboth intentionally skipresetSentSkillNames()to save ~4K tokens/compaction. (Reasonable; not the bug.)dynamicSkillDirs(skills/loadSkillsDir.ts, checked indiscoverSkillDirsForPaths) — gates lazy per-directory discovery. Once a skill dir is in this set, later reads under it return no "new" dir, so nodynamic_skillevent fires and the scoped skill is never re-added. Compaction never clears it. This is the bug.
postCompactCleanup.ts justifies skipping the re-inject with: "dynamic additions are handled by skillChangeDetector / cacheUtils resets." But those only run on filesystem changes / cache clears — never on compaction. So a scoped skill that was discovered but not yet invoked at compaction time falls through every net:
- not in the post-compact
invoked_skillsre-injection (it was never invoked), - not re-listed (
sentSkillNamesfrozen), - not re-discovered (
dynamicSkillDirsfrozen).
Recovery happens only when skillChangeDetector (a chokidar watcher over skill dirs) fires on an incidental file change — hence the non-deterministic, often long-delayed recovery.
Minimal repro
- Start a session whose cwd is above a nested
.claude/skills/directory. Read a file under that subtree → the scoped skill loads and is invocable. - Do a stretch of work that does not touch that subtree, then
/compact(or hit auto-compaction). - Invoke the scoped skill →
Unknown skill. Reading more files in that same subtree does not restore it.
(Observed repeatedly in real long sessions: a scoped deploy skill launched successfully, then after a compaction returned Unknown skill and stayed unavailable for thousands of turns until an unrelated file change re-triggered discovery.)
Suggested fix
Reset dynamicSkillDirs on compaction — it is token-free and simply re-arms the cheap lazy re-discovery so the next in-scope read re-surfaces the skill. (Optionally also reset sentSkillNames, or extend the post-compact invoked_skills re-injection to include discovered-but-not-yet-invoked scoped skills — but clearing dynamicSkillDirs is the minimal, no-token-cost fix.)
Related
- #74990 — broader "compaction drops the Available skills system-reminder" (registry intact). This issue is the scoped/lazy-discovery variant where the registry entry itself is lost.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗