SLOPSHOPPER

sticky-skills

Compaction wipes Claude Code's skill list. sticky-skills puts it back.

prompt
★ 1v0.1.1MITupdated 2026-10-21Jakobbarnwell/sticky-skills/mod
A shopper browsing a rack in a slop shop
README

sticky-skills

[!WARNING] No longer needed. Please uninstall it. Since Claude Code 2.1.304, compaction sends the skill list again by itself (release notes). On those versions sticky-skills makes compactions miss the prompt cache, and it can make a resumed session lose its compaction summary. Uninstall it with claude plugin uninstall sticky-skills@sticky-skills.

Compaction wipes Claude Code's skill map <br/> ... _sticky-skills_ puts it back.

https://github.com/user-attachments/assets/48c25e26-1b26-4412-b986-aaa8b63f8994

It's been a long session. Claude has compacted a few times.

You tell Claude to do something. You've got a few relevant skills it normally finds on its own...

... but now Claude doesn't load the skills.

What happened?

After a compaction, the full list of skills that Claude gets in the system prompt is gone, unless you specifically invoke them.

sticky-skills is a Claude Code mod that puts the full list back right after the compaction summary.

If your workflow requires Claude to find skills automatically without you specifically invoking them, you need this mod.

Install

claude plugin marketplace add Jakobbarnwell/sticky-skills
claude plugin install sticky-skills@sticky-skills

Takes effect in new sessions.

How it works

  • It takes the same "skills catalogue" list that Claude Code gets at the beginning of the session, then adds the full list again as a message right after the compaction summary.
  • When Claude Code later sends a list of its own, only entries the conversation doesn't already hold go through, so the list never appears twice, including in a resumed session.
  • Subagents keep their own list and are left alone.
  • It's the same list Claude Code sends: names and descriptions only. Skill bodies still load only when a skill is used.

Limits:

  • a skill removed mid-session stays in the restored list until the

session ends.

  • Codex doesn't need this: its compacted history keeps the skills.

License

MIT

Source 2 files
hooks/register.ts 90 lines
1import { atom, read, update } from 'claude-code'
2import type { Register, SessionMessage } from 'claude-code'
3
4import type { SkillEntries } from '../types'
5
6// Claude Code sends the full skill list once per process, and after that only
7// what changed, so a compaction loses it. This mod remembers every entry it sees
8// and puts the full list back right after the compaction summary.
9
10const remembered = atom({ plugin: 'sticky-skills', key: 'entries' } as const, {} as SkillEntries)
11
12const HEADER = 'The following skills are available for use with the Skill tool:'
13const REMINDER_START = '<system-reminder>\n'
14const LIST_START = REMINDER_START + HEADER
15const REMINDER_END = '</system-reminder>'
16
17/** Splits a list into its `- name: description` entries, keyed by name. */
18function parseEntries(list: string): SkillEntries {
19  const entries: SkillEntries = {}
20  let current: string | undefined
21  for (const line of list.split('\n')) {
22    const name = /^- ([^\s:]+(?::[^\s:]+)?): /.exec(line)?.[1]
23    if (name !== undefined) {
24      current = name
25      entries[name] = line
26    } else if (current !== undefined) {
27      entries[current] += '\n' + line
28    }
29  }
30  for (const name in entries) entries[name] = entries[name].trimEnd()
31  return entries
32}
33
34function formatList(entries: SkillEntries): string {
35  return [HEADER, '', ...Object.values(entries)].join('\n')
36}
37
38function formatReminder(entries: SkillEntries): string {
39  return `${REMINDER_START}${formatList(entries)}\n${REMINDER_END}`
40}
41
42/** Every entry of the skill lists already in the conversation. */
43function listedEntries(messages: SessionMessage[]): SkillEntries {
44  const lists = messages.flatMap(message =>
45    message.text
46      .split(LIST_START)
47      .slice(1)
48      .map(after => parseEntries(after.split(REMINDER_END)[0])),
49  )
50  return Object.assign({}, ...lists)
51}
52
53export const register: Register = on => {
54  on('prompt.attachment', async ($, e, next) => {
55    const result = await next(e)
56    // Subagents get their own full list, so only the main conversation needs help.
57    if (e.agentId !== undefined || e.type !== 'skill_listing' || result.text === null) return result
58
59    const sent = parseEntries(e.text)
60    await update($, remembered, known => ({ ...known, ...sent }))
61
62    // A resumed session starts a new process, which re-sends the full list even
63    // though the conversation may already hold it. Pass on only what's new.
64    const listed = listedEntries(await $.session.messages())
65    const unlisted = Object.fromEntries(Object.entries(sent).filter(([name, entry]) => listed[name] !== entry))
66    const count = Object.keys(unlisted).length
67    if (count === 0) return { text: null }
68    if (count === Object.keys(sent).length) return result
69    return { text: formatList(unlisted) }
70  })
71
72  on('session.compact', async ($, e, next) => {
73    const result = await next(e)
74    // A precompute only drafts a summary ahead of time; the real compaction comes later.
75    if (e.agentId !== undefined || e.trigger === 'precompute' || 'skip' in result) return result
76
77    // A failure here costs only the restored list, never the compaction.
78    try {
79      const entries = await read($, remembered)
80      if (Object.keys(entries).length === 0) return result
81
82      const [summary, ...kept] = result.messages
83      const reminder = { role: 'user' as const, text: formatReminder(entries), toolUses: [] }
84      return { ...result, messages: [summary, reminder, ...kept] }
85    } catch {
86      return result
87    }
88  })
89}
90
types/index.d.ts 11 lines
1/** Each skill's list entry as Claude Code last sent it, keyed by name. */
2export type SkillEntries = Record<string, string>
3
4declare module 'claude-code' {
5  interface PluginState {
6    'sticky-skills': {
7      entries: SkillEntries
8    }
9  }
10}
11