SLOPSHOPPER

what-skills-am-i-using

Shows which skills, plugins, subagents and MCP tools this conversation has used, in a band above the prompt you can fold open

newbandguardcommandtoast
★ 1v0.1.1MITupdated 2026-10-13DannyW618/What-skills-am-i-using
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · what-skills-am-i-using
› 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 › /what-skills ⎿ what-skills-am-i-using: No skill, plugin, subagent or MCP tool has been used in this conversation yet. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

<h1 align="center"> <img src="assets/logo.svg" width="44" align="absmiddle" alt="">&nbsp;What-skills-am-i-using </h1>

The band folded, then opened into the full order and one row per plugin or MCP server

Install

Type this at the prompt of a terminal claude session:

/plugin install what-skills-am-i-using --marketplace DannyW618/What-skills-am-i-using

Answer y to add the marketplace, then pick a scope. There is nothing to configure.

What you see

One line above the prompt, once the conversation has used something:

◢◤ humanizer → ponytail → navigate → read_page → Explore :.   3 skills · 1 subagent · 2 MCP tools  [ Show ] ×

Press × to close the band for this conversation (it keeps counting). Press Show for the full order and one row per plugin or MCP server:

╭──────────────────────────────────────────────────────────────────────────╮
│ order: humanizer → ponytail → navigate → read_page → Explore             │
│ Built-in subagents           :. Explore ×2                               │
│ claude-in-chrome  MCP server ✗ read_page ×2 (1 failed)  ✓ navigate ×4    │
│ ponytail          plugin     ✓ ponytail ×3                               │
╰──────────────────────────────────────────────────────────────────────────╯
MarkMeaning
a → b → cthe order of use, oldest to newest
bold orange with :. dotsin use right now
✓used, no failures
✗at least one call failed
×3used three times (no number means once)

Long sessions stay compact: the first line keeps the last 8 names, the open box keeps the last 30, and each row shows its 10 most recent entries. /what-skills prints everything.

What it counts

WhatHow it is noticed
Skills Claude invokescalls to the Skill tool
Skills you type (/name)slash commands that are not built in
Subagentscalls to the Agent tool, by agent type
MCP toolsany tool named mcp__server__tool

Command

  • /what-skills prints the full list as a tree.
  • /what-skills hide and /what-skills show close the band and bring it back.
  • /what-skills reset clears the counts.

Good to know

  • "In use" is a best guess for skills. The mod sees a skill being loaded, not whether Claude keeps following it, so a skill counts as in use until the turn that loaded it ends.
  • It only watches. It never blocks or changes a tool call.
  • It writes nothing to disk and sends nothing over the network. The counts cover the current conversation and go away on /clear.
  • Built and tested on Claude Code 2.1.295. The mod API is early access and may change between releases.

License

MIT

Source 4 files
hooks/register.tsx 170 lines
1// What-skills-am-i-using: counts skill, subagent and MCP tool use into $.state and draws it as a band above the prompt.
2import { atom, read, update } from 'claude-code'
3import type { EngineInterface, Register } from 'claude-code'
4
5import { SKILLS, bump, chip, classify, groups, order, split, summary, treeText } from './usage'
6import type { Hit } from './usage'
7
8const NAME = 'what-skills-am-i-using'
9const CMD = 'what-skills'
10// The open band shows the most recent groups and entries; /what-skills prints the rest.
11const MAX_GROUPS = 8
12const MAX_ITEMS = 10
13const MAX_ORDER = 8
14const MAX_HISTORY = 30
15const usesAtom = atom({ plugin: 'what-skills-am-i-using', key: 'uses' } as const, {})
16const openAtom = atom({ plugin: 'what-skills-am-i-using', key: 'isOpen' } as const, false)
17const hiddenAtom = atom({ plugin: 'what-skills-am-i-using', key: 'isHidden' } as const, false)
18// The newest `seq` when the running turn began: anything above it was used in this turn.
19const markAtom = atom({ plugin: 'what-skills-am-i-using', key: 'turnMark' } as const, 0)
20
21// An observer must never break the event it watches: log the failure and carry on.
22const safe = <T,>($: EngineInterface, p: Promise<T>) =>
23  p.catch(err => { $.ui.log(`${NAME}: ${String(err)}`, { to: 'debug' }); return undefined })
24
25export const register: Register = on => {
26  on('session.start', async ($, e, next) => {
27    await safe($, (async () => {
28      await $.command.register({ name: 'what-skills', description: 'List the skills, plugins, subagents and MCP tools this conversation has used', argumentHint: '[reset|hide|show]' })
29      // A resumed conversation (or a mod installed mid-way) starts from what the transcript already holds.
30      // shortcut: only the main conversation's rows are replayed, upgrade if past subagents' own calls matter after a resume.
31      const rows = await $.session.messages()
32      await update($, usesAtom, cur => {
33        if (Object.keys(cur).length) return Object.fromEntries(Object.entries(cur).map(([k, u]) => [k, { ...u, running: 0 }]))
34        let uses = cur
35        for (const row of rows) for (const t of row.toolUses) {
36          const hit = classify(t.tool, t.input)
37          if (hit) uses = bump(uses, hit, { count: 1 })
38        }
39        return uses
40      })
41    })())
42    return next(e)
43  })
44
45  on('turn.start', async ($, e, next) => {
46    await safe($, (async () => {
47      const seq = Math.max(0, ...Object.values(await read($, usesAtom)).map(u => u.seq))
48      await update($, markAtom, () => seq)
49    })())
50    return next(e)
51  })
52
53  on('tool.call', async ($, e, next) => {
54    const hit = classify(String(e.tool), e as unknown as Record<string, unknown>)
55    if (!hit) return next(e)
56    await safe($, update($, usesAtom, u => bump(u, hit, { count: 1, running: 1 })))
57    let failed = true
58    try {
59      const r = await next(e)
60      failed = !!r.isError
61      return r
62    } finally {
63      await safe($, update($, usesAtom, u => bump(u, hit, { running: -1, errors: failed ? 1 : 0 })))
64    }
65  }).catch(($, e, next) => next(e))
66
67  // A skill the person types as /name never goes through the Skill tool.
68  on('command.run', async ($, e, next) => {
69    if (e.command !== CMD) {
70      await safe($, (async () => {
71        const info = (await $.command.list()).find(c => c.name === e.command)
72        if (!info || info.source === 'builtin') return
73        const hit: Hit = { kind: 'skill', ...split(e.command, info.plugin ?? SKILLS) }
74        await update($, usesAtom, u => bump(u, hit, { count: 1 }))
75      })())
76    }
77    return next(e)
78  }).catch(($, e, next) => next(e))
79
80  on('command.run', { command: 'what-skills' }, async ($, e) => {
81    const arg = e.args.trim()
82    if (arg === 'hide' || arg === 'show') {
83      await update($, hiddenAtom, () => arg === 'hide')
84      return { text: arg === 'hide' ? 'Band hidden; it keeps counting. /what-skills show brings it back.' : 'Band shown.' }
85    }
86    if (arg === 'reset') {
87      await update($, usesAtom, () => ({}))
88      return { text: 'Cleared. Counting starts again from here.' }
89    }
90    const uses = await read($, usesAtom)
91    return { text: Object.keys(uses).length ? `${summary(uses)}\n\n${treeText(uses)}` : 'No skill, plugin, subagent or MCP tool has been used in this conversation yet.' }
92  })
93
94  // The band: the order of use ending at the latest, counts at the right; open, the longer order and one row per plugin or MCP server. Other mods' bands stay above it.
95  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
96    const uses = await read($, usesAtom)
97    const below = await next(e)
98    if (e.props.hasSurvey || !Object.keys(uses).length || (await read($, hiddenAtom))) return below
99    const isOpen = await read($, openAtom)
100    const E = $.ui.resolve(e)
101    const { Box, Button, Text } = E
102    // Motion needs a surface with client modules (terminal, desktop); elsewhere the dots stand still.
103    const Client = 'Client' in E ? E.Client : undefined
104    const dots = (key: string) => (Client ? <Client key={key} module="./motion.tsx" props={{}} /> : <Text color="claude">:.</Text>)
105    const all = order(uses)
106    const last = all[all.length - 1]!
107    // Lit only while it is true: a call is in flight, or the latest was used in the turn still running. The mod cannot see more than that.
108    const isActive = all.some(u => u.running) || (e.props.isWorking && last.seq > (await read($, markAtom)))
109    const counts = summary(uses)
110    // The chain ends at the latest use and reaches back as far as the line has room for.
111    const room = e.props.bodyColumns - counts.length - 20
112    const shown = [last]
113    for (let i = all.length - 2, used = last.name.length + 4; i >= 0 && shown.length < MAX_ORDER; i -= 1) {
114      used += all[i]!.name.length + 3
115      if (used > room) break
116      shown.unshift(all[i]!)
117    }
118    const gs = groups(uses)
119    const wide = Math.min(24, Math.max(...gs.map(g => g.name.length)))
120    return (
121      <Box flexDirection="column">
122        {below}
123        <Box justifyContent="space-between">
124          <Box>
125            {/* The mod's own mark: two triangles in two tones, the shape of assets/logo.svg. */}
126            <Text><Text color="claude">◢</Text><Text bold>◤</Text> </Text>
127            <Text dimColor>{`${shown.length < all.length ? '… → ' : ''}${shown.slice(0, -1).map(u => `${u.name} → `).join('')}`}</Text>
128            <Text bold={isActive} color={isActive ? 'claude' : undefined} dimColor={!isActive}>{last.name}</Text>
129            {isActive && <Text> </Text>}
130            {isActive && dots('dots-head')}
131          </Box>
132          <Box gap={2} flexShrink={0}>
133            <Text dimColor>{counts}</Text>
134            <Button key="toggle" label={isOpen ? 'Hide' : 'Show'} onPress={() => update($, openAtom, was => !was)} />
135            <Button key="close" label="×" plain role="dismiss" onPress={async () => { await update($, hiddenAtom, () => true); $.ui.toast('Band closed for this conversation; it keeps counting. /what-skills show brings it back.') }} />
136          </Box>
137        </Box>
138        {isOpen && (
139          <Box flexDirection="column" borderStyle="round" borderDimColor paddingX={1}>
140            <Text dimColor>
141              {`order: ${all.length > MAX_HISTORY ? '… → ' : ''}${all.slice(-MAX_HISTORY, -1).map(u => `${u.name} → `).join('')}`}
142              {isActive ? <Text bold color="claude" dimColor={false}>{last.name}</Text> : last.name}
143            </Text>
144            {gs.slice(0, MAX_GROUPS).map(g => (
145              <Box>
146                <Box width={wide + 12} flexShrink={0}>
147                  <Text bold>{g.name.slice(0, wide).padEnd(wide + 1)}</Text>
148                  <Text dimColor>{g.label}</Text>
149                </Box>
150                <Box flexWrap="wrap" columnGap={2}>
151                  {g.items.slice(0, MAX_ITEMS).map(u => (
152                    <Box gap={1}>
153                      {u.running ? dots(`dots-${u.kind}-${g.name}-${u.name}`) : <Text color={u.errors ? 'error' : 'success'}>{u.errors ? '✗' : '✓'}</Text>}
154                      <Text bold={isActive && u === last}>{chip(u, g)}</Text>
155                    </Box>
156                  ))}
157                  {g.items.length > MAX_ITEMS && <Text dimColor>{`+${g.items.length - MAX_ITEMS} more`}</Text>}
158                </Box>
159              </Box>
160            ))}
161            {(gs.length > MAX_GROUPS || gs.some(g => g.items.length > MAX_ITEMS)) && (
162              <Text dimColor>{`${gs.length > MAX_GROUPS ? `+${gs.length - MAX_GROUPS} more groups · ` : ''}/what-skills lists everything`}</Text>
163            )}
164          </Box>
165        )}
166      </Box>
167    )
168  })
169}
170
hooks/usage.ts 70 lines
1// What a tool call or a typed command says about skill, subagent and MCP use. Pure, so the band, the command and the tests share it.
2import type { Kind, Use, Uses } from '../types'
3
4export type Hit = Pick<Use, 'group' | 'kind' | 'name'>
5
6// Groups for things no plugin owns.
7export const SKILLS = 'Your skills'
8export const AGENTS = 'Built-in subagents'
9
10const NOUN: Record<Kind, string> = { skill: 'skill', agent: 'subagent', mcp: 'MCP tool' }
11
12// `plugin:name` belongs to that plugin; a bare name goes to `fallback`.
13export const split = (id: string, fallback: string) => {
14  const i = id.indexOf(':')
15  return i > 0 ? { group: id.slice(0, i), name: id.slice(i + 1) } : { group: fallback, name: id }
16}
17
18export function classify(tool: string, input: Record<string, unknown>): Hit | null {
19  if (tool === 'Skill' && typeof input.skill === 'string') return { kind: 'skill', ...split(input.skill.replace(/^\//, ''), SKILLS) }
20  if (tool === 'Agent') return { kind: 'agent', ...split(typeof input.subagent_type === 'string' ? input.subagent_type : 'general-purpose', AGENTS) }
21  const mcp = /^mcp__(.+?)__(.+)$/.exec(tool)
22  return mcp ? { kind: 'mcp', group: mcp[1]!, name: mcp[2]! } : null
23}
24
25// Add to one entry's numbers; a new use (`count`) also makes it the most recent.
26export function bump(uses: Uses, hit: Hit, d: { count?: number; errors?: number; running?: number }): Uses {
27  const key = `${hit.kind}:${hit.group}/${hit.name}`
28  const cur = uses[key] ?? { ...hit, count: 0, errors: 0, running: 0, seq: 0 }
29  const seq = d.count ? 1 + Math.max(0, ...Object.values(uses).map(u => u.seq)) : cur.seq
30  return { ...uses, [key]: { ...cur, count: cur.count + (d.count ?? 0), errors: cur.errors + (d.errors ?? 0), running: Math.max(0, cur.running + (d.running ?? 0)), seq } }
31}
32
33export type Group = { name: string; label: string; items: Use[] }
34
35// The nested list: one group per plugin or MCP server, most recently used first, and the same inside each group.
36export function groups(uses: Uses): Group[] {
37  const by = new Map<string, Use[]>()
38  for (const u of Object.values(uses)) by.set(u.group, [...(by.get(u.group) ?? []), u])
39  return [...by]
40    .map(([name, items]) => ({
41      name,
42      label: name === SKILLS || name === AGENTS ? '' : items.every(u => u.kind === 'mcp') ? 'MCP server' : 'plugin',
43      items: items.sort((a, b) => b.seq - a.seq),
44    }))
45    .sort((a, b) => b.items[0]!.seq - a.items[0]!.seq)
46}
47
48// Everything used, oldest first, each at the place of its most recent use; the last one is the latest.
49export const order = (uses: Uses) => Object.values(uses).sort((a, b) => a.seq - b.seq)
50
51// One entry on a group's row. Inside a plugin, anything that is not a skill says what it is.
52export const chip = (u: Use, g: Group) =>
53  `${g.label === 'plugin' && u.kind !== 'skill' ? (u.kind === 'agent' ? 'agent ' : 'tool ') : ''}${u.name}${u.count > 1 ? ` ×${u.count}` : ''}${u.errors ? ` (${u.errors} failed)` : ''}`
54
55// "2 skills · 1 subagent · 4 MCP tools": how many different ones, not how many calls.
56export const summary = (uses: Uses) =>
57  (Object.keys(NOUN) as Kind[])
58    .map(k => [Object.values(uses).filter(u => u.kind === k).length, NOUN[k]] as const)
59    .filter(([n]) => n > 0)
60    .map(([n, noun]) => `${n} ${noun}${n > 1 ? 's' : ''}`)
61    .join(' · ')
62
63const branch = (g: Group, i: number) => (i === g.items.length - 1 ? '└─' : '├─')
64const itemText = (u: Use) => `${NOUN[u.kind].padEnd(8)} ${u.name}  ×${u.count}`
65
66export const treeText = (uses: Uses) =>
67  groups(uses)
68    .map(g => [`${g.name}${g.label ? ` (${g.label})` : ''}`, ...g.items.map((u, i) => `${branch(g, i)} ${itemText(u)}${u.errors ? `  ✗${u.errors}` : ''}`)].join('\n'))
69    .join('\n')
70
hooks/motion.tsx 16 lines
1// Client surface module for the band's one moving part: dots that build up while Claude is working.
2import type { ClientModule } from 'claude-code'
3
4const FRAMES = ['.  ', ':  ', ':. ', ':: ', '::.', ':::']
5
6const Motion: ClientModule<{}, number> = (_p, s) => {
7  const { Text } = s.elements
8  if (s.state === undefined) {
9    s.every(220, () => s.setState((s.state ?? 0) + 1))
10    s.setState(0)
11  }
12  return <Text bold color="claude">{FRAMES[(s.state ?? 0) % FRAMES.length]}</Text>
13}
14
15export default Motion
16
types/index.d.ts 13 lines
1export type Kind = 'skill' | 'agent' | 'mcp'
2
3// One skill, subagent type or MCP tool the conversation has used; `group` is the plugin or MCP server it belongs to.
4export type Use = { group: string; kind: Kind; name: string; count: number; errors: number; running: number; seq: number }
5
6export type Uses = Record<string, Use>
7
8declare module 'claude-code' {
9  interface PluginState {
10    'what-skills-am-i-using': { uses: Uses; isOpen: boolean; isHidden: boolean; turnMark: number }
11  }
12}
13