Alerts you, and reminds Claude, to activate a superpowers skill when a prompt looks like development work

A Claude Code mod. When a prompt looks like development work it shows a band above the prompt suggesting the matching superpowers skill, with an Activate button that fills the prompt with the skill command, and attaches a short reminder to Claude to invoke that skill first.
At the prompt of a Claude Code terminal session:
/plugin install thinkfirst-skillcue --marketplace DCXTribe/claude-code-think-first
From a shell:
claude plugin install thinkfirst-skillcue --marketplace DCXTribe/claude-code-think-first
In the desktop app: click + next to the prompt box, then Plugins, Add marketplace with DCXTribe/claude-code-think-first, then Add plugin.
claude plugin disable thinkfirst-skillcue
claude plugin enable thinkfirst-skillcue
Inside a session: /skillcue on|off|pause|resume|status, or the "Skill cue" row in /config (terminal only).
From a clone, run it from its folder with claude --plugin-dir plugins/thinkfirst-skillcue.
claude plugin validate .
claude plugin test .
/skillcue resume turns it back on.Other bands above the prompt (thinkfirst-ambition's, for one) are drawn beneath this one rather than hidden.
/skillcue on|off writes its one option, enabled, to your Claude Code settings.hooks/register.tsx 171 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Suggestion } from '../types'
5
6// Which superpowers skill fits the prompt, by the words it uses.
7// A failure described, not the bare verb "fix": "fix the typo in the README"
8// or "fix all of the review findings" is not a bug hunt.
9const DEBUG = /\b(bug|bugs|broken|crash|crashes|crashing|error|errors|exception|exceptions|failing|fails|failed|not working|doesn'?t work|regression|debug|debugging|stack ?trace|traceback|segfault)\b/i
10// Verbs that only ever mean code.
11const CODE_VERB = /\b(implement|implements|implementing|refactor|refactors|refactoring|scaffold|scaffolding|migrate|migrating)\b/i
12// Verbs that mean code only next to a thing software is made of: "write a
13// LinkedIn post" and "create a calendar invite" are not dev work.
14const BUILD_VERB = /\b(build|builds|building|create|creates|creating|add|adds|adding|write|writes|writing|develop|developing|integrate|integrating|set ?up|wire ?up|extend|extending)\b/i
15const CODE_NOUN = /\b(feature|features|component|components|endpoint|endpoints|api|apis|cli|app|apps|service|services|module|modules|function|functions|class|classes|script|scripts|page|pages|screen|screens|plugin|plugins|mod|mods|hook|hooks|database|schema|migration|route|routes|handler|library|sdk|server|backend|frontend|website|toggle|setting|settings|button|form|widget)\b/i
16const TEST = /\b(test|tests|testing|spec|specs|coverage|tdd)\b/i
17// Prompts that are not dev tasks even when a dev word appears.
18const NOT_DEV = /^\s*(\/|what is|what's|explain|why does|how does|how do i|tell me about|show me|is there|are there|do i have)/i
19
20const CODE_FILE = /\.(ts|tsx|js|jsx|mjs|cjs|py|rb|go|rs|java|kt|swift|m|mm|c|cc|cpp|h|hpp|cs|php|sh|zsh|sql|vue|svelte|dart|scala|ex|exs|html|css|scss)$/i
21
22const COMMAND = 'skillcue'
23const CONFIG_KEY = 'thinkfirst-skillcue.enabled'
24
25const nudge = atom({ plugin: 'thinkfirst-skillcue', key: 'nudge' } as const, null)
26const paused = atom({ plugin: 'thinkfirst-skillcue', key: 'paused' } as const, false)
27const PAUSED_STATUS = `skill cue paused for this session. /skillcue resume to continue`
28
29export function classify(text: string): Suggestion | null {
30 if (NOT_DEV.test(text)) return null
31 if (DEBUG.test(text)) {
32 return { skill: 'superpowers:systematic-debugging', reason: 'bug or failure described' }
33 }
34 if (CODE_VERB.test(text) || (BUILD_VERB.test(text) && CODE_NOUN.test(text))) {
35 return { skill: 'superpowers:brainstorming', reason: 'something to build or change' }
36 }
37 if (TEST.test(text)) {
38 return { skill: 'superpowers:test-driven-development', reason: 'tests mentioned' }
39 }
40 return null
41}
42
43export const register: Register = (on, options) => {
44 const enabled = options.enabled !== false
45 let active: string | null = null
46 let suggestion: Suggestion | null = null
47 let warnedEdit = false
48
49 on('session.start', async ($, e, next) => {
50 await $.command.register({
51 name: COMMAND,
52 description: 'Skill cue: /skillcue on|off|pause|resume|status',
53 })
54 return next(e)
55 })
56
57 on('command.run', { command: COMMAND }, async ($, e) => {
58 const arg = (e.args ?? '').trim().toLowerCase()
59 if (arg === 'on' || arg === 'off') {
60 await $.config.set({ key: CONFIG_KEY, value: arg === 'on' })
61 return { text: `Skill cue turned ${arg}.` }
62 }
63 if (arg === 'pause' || arg === 'hold') {
64 await update($, paused, () => true)
65 await update($, nudge, () => null)
66 $.ui.status(PAUSED_STATUS)
67 return { text: 'Skill cue paused for this session. /skillcue resume to continue.' }
68 }
69 if (arg === 'resume') {
70 await update($, paused, () => false)
71 $.ui.status(undefined)
72 return { text: 'Skill cue resumed.' }
73 }
74 const isPaused = await read($, paused)
75 const state = !enabled ? 'off' : isPaused ? 'paused for this session' : 'on'
76 return {
77 text: `Skill cue is ${state}. Use /${COMMAND} on|off|pause|resume, or the "Skill cue" row in /config.`,
78 }
79 })
80
81 on('prompt.submit', async ($, e, next) => {
82 active = null
83 warnedEdit = false
84 const isPaused = await read($, paused)
85 suggestion = enabled && !isPaused ? classify(e.text) : null
86 await update($, nudge, () => suggestion)
87
88 // The last turn's "active" is not this turn's.
89 $.ui.status(isPaused ? PAUSED_STATUS : undefined)
90 if (suggestion === null) return next(e)
91
92 $.ui.toast(`Dev task detected (${suggestion.reason}): activate ${suggestion.skill}`, { timeoutMs: 6000 })
93
94 const reminder =
95 `[skillcue] This prompt looks like development work (${suggestion.reason}). ` +
96 `Before any other response or tool call, invoke the Skill tool with "${suggestion.skill}" ` +
97 `(or the superpowers skill that fits better), as the using-superpowers rule requires.`
98
99 return next({ ...e, context: [...(e.context ?? []), reminder] })
100 })
101
102 on('tool.call', { tool: 'Skill' }, async ($, e, next) => {
103 if (e.skill.startsWith('superpowers:')) {
104 active = e.skill
105 await update($, nudge, () => null)
106 $.ui.status(`superpowers: ${e.skill.slice('superpowers:'.length)} active`)
107 }
108 return next(e)
109 })
110
111 // The toast to show for an edit of `path` before superpowers is active, once per turn.
112 const editWarning = (path: string): string | null => {
113 if (suggestion === null || active !== null || warnedEdit || !CODE_FILE.test(path)) return null
114 warnedEdit = true
115 return `Editing code without superpowers active. Suggested: ${suggestion.skill}`
116 }
117
118 on('tool.call', { tool: 'Edit' }, ($, e, next) => {
119 const warning = editWarning(e.file_path)
120 if (warning !== null) $.ui.toast(warning, { timeoutMs: 8000 })
121 return next(e)
122 }).catch(($, e, next) => next(e))
123
124 on('tool.call', { tool: 'Write' }, ($, e, next) => {
125 const warning = editWarning(e.file_path)
126 if (warning !== null) $.ui.toast(warning, { timeoutMs: 8000 })
127 return next(e)
128 }).catch(($, e, next) => next(e))
129
130 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
131 const current = await read($, nudge)
132 if (e.props.hasSurvey || current === null) {
133 return next(e)
134 }
135
136 const { Box, Button, Text } = $.ui.resolve(e)
137 const clear = () => update($, nudge, () => null)
138 // Put the skill command in front of whatever is being typed, never over it.
139 const activate = async () => {
140 const draft = await $.prompt.read().then((b) => b.text.trim(), () => '')
141 await $.prompt.fill({ text: `/${current.skill} ${draft}`, mode: 'replace' })
142 await clear()
143 }
144 const hold = async () => {
145 await update($, paused, () => true)
146 await clear()
147 $.ui.status(PAUSED_STATUS)
148 }
149
150 // Another plugin's band (thinkfirst-ambition's, say) may sit beneath: draw it
151 // under this one instead of hiding it.
152 const below = await next(e)
153
154 return (
155 <Box flexDirection="column">
156 <Box key="nudge" borderStyle="round" borderColor="claude" paddingX={1} gap={1}>
157 <Text bold color="claude">⚡ Skill cue</Text>
158 <Text>
159 Dev task: {current.reason}. Suggested <Text bold>{current.skill}</Text>
160 </Text>
161 <Button key="activate" variant="primary" hotkey="a" onPress={activate}>Activate</Button>
162 {/* role="dismiss" is the desktop's close control: it must only dismiss, never pause the session. */}
163 <Button key="dismiss" role="dismiss" dimColor hotkey="d" onPress={clear}>Dismiss</Button>
164 <Button key="hold" dimColor hotkey="h" onPress={hold}>Hold</Button>
165 </Box>
166 {below}
167 </Box>
168 )
169 })
170}
171types/index.d.ts 11 lines1export type Suggestion = {
2 skill: string
3 reason: string
4}
5
6declare module 'claude-code' {
7 interface PluginState {
8 'thinkfirst-skillcue': { nudge: Suggestion | null; paused: boolean }
9 }
10}
11