SLOPSHOPPER

skills-band

One-row skills band above the prompt: theme-colored labels, Claude-orange dots on active skills and caveman/ponytail modes

newbandprompttimer
★ 1v0.5.1MITupdated 2026-10-13codehamster69/skills-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · skills-band
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM SKILLS no skills used yet ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
SKILLS no skills used yet
README

skills-band

A one-row band above the Claude Code prompt that shows which skills this session has used.

skills-band above the Claude Code prompt

<sub>Preview of the band in a dark theme: caveman and ponytail active, seo-audit loading this turn (spinning), the rest used in earlier turns. Labels take your theme's text color, so a light theme shows dark text.</sub>

  • Every skill counts: skills typed as /name, loaded through the Skill tool, or preloaded into subagents all show up. Plugin skills show their short name (frontend-design, not frontend-design:frontend-design).
  • Active dot: an orange ● in Claude's color marks a skill used this turn. Skills from earlier turns have no dot and are dimmed. The most recent skills come first. ×N counts how many times a skill ran.
  • Theme colors: labels use your theme's own text color, so the band stays readable in light and dark themes.
  • Pulse: a skill loaded during the current turn spins ◐◓◑◒ while Claude works, then shows ● when the turn ends.
  • Mode badges (optional): when the caveman or ponytail plugins are active in this session, they show with an orange dot and their mode (ponytail:ultra). When they stop, or aren't installed, they disappear.

Install

In Claude Code:

/plugin marketplace add codehamster69/skills-band
/plugin install skills-band@rohan-mods

Or from a shell:

claude plugin marketplace add codehamster69/skills-band
claude plugin install skills-band@rohan-mods

Restart Claude Code. The band appears above the prompt right away ("no skills used yet") and fills in as skills run. Skills that ran before the restart are not shown.

Using caveman/ponytail?

The band already shows their badges. If you had a statusLine in ~/.claude/settings.json that only showed those badges, you can remove it to avoid seeing them twice.

Update / remove

claude plugin update skills-band@rohan-mods
claude plugin uninstall skills-band@rohan-mods

Requirements

  • A Claude Code build that supports mods (hooks/hooks.json with modules).
  • A font with ◐◓◑◒● (the Windows Terminal, iTerm2 and most Linux defaults have them).
  • Hex background colors, which need a truecolor terminal.

Develop

claude plugin validate .
claude plugin test .

The dot color is the ORANGE constant at the top of hooks/register.tsx; everything else follows your theme.

License

MIT

Source 2 files
hooks/register.tsx 155 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer } from 'claude-code'
3
4import type { Modes, Skill } from '../types'
5
6const used = atom({ plugin: 'skills-band', key: 'used' } as const, [] as Skill[])
7const turn = atom({ plugin: 'skills-band', key: 'turn' } as const, 0)
8const frame = atom({ plugin: 'skills-band', key: 'frame' } as const, 0)
9// whether the session's start reached this module; a mod loaded later never sees ponytail's one announcement
10const sawStart = atom({ plugin: 'skills-band', key: 'sawStart' } as const, false)
11const modes = atom({ plugin: 'skills-band', key: 'modes' } as const, { caveman: null, ponytail: null } as Modes)
12
13const SPIN = ['◐', '◓', '◑', '◒']
14// Claude's own orange marks what is active; everything else takes the surface's text color
15const ORANGE = '#d97757'
16
17// dot: an orange mark before the label, spin while its turn runs; dim: used in an earlier turn
18type Item = { key: string; text: string; dot: 'on' | 'spin' | null; dim: boolean }
19
20// the caveman and ponytail hooks say so in the context they inject; that text, not a flag file, proves a mode reached this session
21const MARKS = {
22  caveman: /CAVEMAN MODE ACTIVE(?: — mode: | \()([a-z0-9-]{1,20})/,
23  ponytail: /PONYTAIL MODE ACTIVE — level: ([a-z0-9-]{1,20})/,
24} as const
25
26const injected = (context: readonly string[] | undefined, name: keyof typeof MARKS) =>
27  (context ?? []).join('\n').match(MARKS[name])?.[1] ?? null
28
29// ponytail injects once at session start, so a later /ponytail level switch is read from its flag; whitelist the text
30const ponytailFlag = async ($: EngineInterface) => {
31  try {
32    const config = await $.env.get('CLAUDE_CONFIG_DIR')
33    const home = config ?? `${(await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME')) ?? ''}/.claude`
34    const mode = String(await $.fs.read(`${home}/.ponytail-active`)).split(/\r?\n/)[0].trim()
35    return /^[a-z0-9-]{1,20}$/.test(mode) ? mode : null
36  } catch {
37    return null
38  }
39}
40
41// loaded after the session started (a reload, the desktop's late load): ponytail's flag is the only sign left
42const ponytailFallback = async ($: EngineInterface) => {
43  const isLate = !(await read($, sawStart))
44  if (!isLate && (await read($, modes)).ponytail === null) return
45  const level = await ponytailFlag($)
46  // the flag only relabels a ponytail this session already has, unless the start was missed
47  if (level !== null || isLate) await update($, modes, m => ({ ...m, ponytail: level }))
48}
49
50export const register: Register = on => {
51  let spinner: Timer | undefined
52
53  on('classic.SessionStart', async ($, e, next) => {
54    const ran = await next(e)
55    const context = (ran as { additionalContext?: string[] }).additionalContext
56    await update($, sawStart, () => true)
57    await update($, modes, () => ({ caveman: injected(context, 'caveman'), ponytail: injected(context, 'ponytail') }))
58    return ran
59  })
60
61  on('session.start', async ($, e, next) => {
62    const ran = await next(e)
63    await ponytailFallback($)
64    return ran
65  })
66
67  // caveman re-injects on every prompt while it is on, so the latest prompt decides its badge
68  on('classic.UserPromptSubmit', async ($, e, next) => {
69    const ran = await next(e)
70    const caveman = injected((ran as { additionalContext?: string[] }).additionalContext, 'caveman')
71    await update($, modes, m => ({ ...m, caveman }))
72    return ran
73  })
74
75  on('prompt.submit', async ($, e, next) => {
76    await update($, turn, n => n + 1)
77    const ran = await next(e)
78    await ponytailFallback($)
79    return ran
80  })
81
82  // skill.prompt fires for /name, the Skill tool and subagent preloads alike; tool.call saw only the Skill tool
83  on('skill.prompt', async ($, e, next) => {
84    const ran = await next(e)
85    const now = await read($, turn)
86    await update($, used, list => {
87      const prev = list.find(s => s.name === e.skill)
88      return [...list.filter(s => s.name !== e.skill), { name: e.skill, turn: now, count: (prev?.count ?? 0) + 1 }]
89    })
90    try {
91      spinner ??= $.clock.every(120, () => void update($, frame, f => f + 1))
92    } catch {
93      // no timers on this host: the spinner shows one frame instead of turning
94    }
95    return ran
96  })
97
98  on('turn.complete', async ($, e, next) => {
99    spinner?.cancel()
100    spinner = undefined
101    return next(e)
102  })
103
104  // above the prompt: the desktop draws no plugin tree in the hint line under it, so this band is the only place it shows
105  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
106    if (e.props.hasSurvey) {
107      return next(e)
108    }
109
110    const now = await read($, turn)
111    const tick = await read($, frame)
112    const m = await read($, modes)
113    const skills = [...(await read($, used))].sort((a, b) => b.turn - a.turn)
114    const always = (['caveman', 'ponytail'] as const).flatMap(name => {
115      const mode = m[name]
116      if (mode === null) return []
117      return [{ name, label: mode === 'full' || mode === name ? name : `${name}:${mode}` }]
118    })
119
120    // caveman and ponytail are only listed while their hooks inject, so a listed one is always active
121    const items: Item[] = [
122      ...always.map(a => ({ key: a.name, text: a.label, dot: 'on' as const, dim: false })),
123      ...skills.map(s => {
124        const isFresh = now === s.turn
125        // plugin skills arrive as plugin:skill; the plugin half is noise in the band
126        const label = s.name.split(':').pop() ?? s.name
127        return {
128          key: s.name,
129          text: `${label}${s.count > 1 ? ` ×${s.count}` : ''}`,
130          dot: isFresh ? (e.props.isWorking ? ('spin' as const) : ('on' as const)) : null,
131          dim: !isFresh,
132        }
133      }),
134    ]
135    const empty = skills.length === 0 ? 'no skills used yet' : null
136
137    // theme elements on every surface: an Svg cannot see the app's theme, so its text went unreadable when the two disagreed
138    const { Box, Text } = $.ui.resolve(e)
139    // the terminal's own 'claude' key is its theme's orange; elsewhere Claude's brand orange
140    const orange = e.surface === 'terminal' ? 'claude' : ORANGE
141    return (
142      <Box flexDirection="row" flexWrap="wrap" alignItems="center" columnGap={2}>
143        <Text key="header" bold>SKILLS</Text>
144        {items.map(it => (
145          <Box key={it.key} flexDirection="row" columnGap={1}>
146            {it.dot && <Text color={orange}>{it.dot === 'spin' ? SPIN[tick % SPIN.length] : '●'}</Text>}
147            <Text dimColor={it.dim}>{it.text}</Text>
148          </Box>
149        ))}
150        {empty && <Text key="empty" dimColor>{empty}</Text>}
151      </Box>
152    )
153  })
154}
155
types/index.d.ts 9 lines
1export type Skill = { name: string; turn: number; count: number }
2export type Modes = { caveman: string | null; ponytail: string | null }
3
4declare module 'claude-code' {
5  interface PluginState {
6    'skills-band': { used: Skill[]; turn: number; frame: number; modes: Modes; sawStart: boolean }
7  }
8}
9