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

<h1 align="center"> <img src="assets/logo.svg" width="44" align="absmiddle" alt=""> What-skills-am-i-using </h1>
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.
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 │
╰──────────────────────────────────────────────────────────────────────────╯
| Mark | Meaning |
|---|---|
a → b → c | the order of use, oldest to newest |
bold orange with :. dots | in use right now |
✓ | used, no failures |
✗ | at least one call failed |
×3 | used 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 | How it is noticed |
|---|---|
| Skills Claude invokes | calls to the Skill tool |
Skills you type (/name) | slash commands that are not built in |
| Subagents | calls to the Agent tool, by agent type |
| MCP tools | any tool named mcp__server__tool |
/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./clear.hooks/register.tsx 170 lines1// 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}
170hooks/usage.ts 70 lines1// 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')
70hooks/motion.tsx 16 lines1// 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
16types/index.d.ts 13 lines1export 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