SLOPSHOPPER

thinkfirst-skillcue

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

bandguardcommandtoaststatusprompt
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · thinkfirst-skillcue
› fix the failing auth test and add an audit log call ╭────────────────────────────────────────────╮ │ thinkfirst-skillcue │ ⏺ Read(src/auth.ts) │ Dev task detected (bug or failure │ ⎿ Read 6 lines │ described): activate │ ⏺ Update(src/auth.ts) │ superpowers:systematic-debugging │ ⎿ Added 2 lines, removed 1 line ╰────────────────────────────────────────────╯ ⏺ Bash(bun test) ╭────────────────────────────────────────────╮ ⎿ 3 pass, 1 fail │ thinkfirst-skillcue │ │ Editing code without superpowers active. │ ● Done. refresh now rejects expired claims and logs an audit event. │ Suggested: │ │ superpowers:systematic-debugging │ ✻ Worked for 42s · done 4:20 PM ╰────────────────────────────────────────────╯ › /skillcue ⎿ thinkfirst-skillcue: Skill cue is on. Use /skillcue on|off|pause|resume, or the "Skill cue" row in /config. ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ ⚡ Skill cue Dev task: bug or failure described. Suggested superpowers:systematic-debugging [ Activate ] [ Dismiss ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
╭─────────────────────────────────────────────────────────────────────────────────────────────────── │ ⚡ Skill cue Dev task: bug or failure described. Suggested superpowers:systematic-debugging [ Activ ╰─────────────────────────────────────────────────────────────────────────────────────────────────── ⟨Claude Code's own drawing⟩
README

thinkfirst-skillcue

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.

Install

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.

Manage

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).

Develop

From a clone, run it from its folder with claude --plugin-dir plugins/thinkfirst-skillcue.

claude plugin validate .
claude plugin test .

The band

  • Activate puts the suggested skill command in front of whatever you are typing; press Enter to run it.
  • Dismiss hides the band for this prompt (on the desktop it is the band's close control).
  • Hold pauses detection for the rest of the session, for when superpowers is already running. /skillcue resume turns it back on.

Other bands above the prompt (thinkfirst-ambition's, for one) are drawn beneath this one rather than hidden.

What it runs and sends

  • Prompt check. It matches each prompt you submit against fixed word lists, on your machine. Nothing is sent anywhere to decide.
  • Context for Claude. When a prompt looks like development work, it attaches one line of context to that prompt telling Claude to invoke the suggested superpowers skill first.
  • Watching tools. It notices when Claude calls the Skill tool (to show which superpowers skill is active) and when Claude edits a code file before one is active (to show a toast). It never blocks or changes those calls.
  • Settings. /skillcue on|off writes its one option, enabled, to your Claude Code settings.
  • Nothing else. It makes no model calls, reads no files, runs no processes and contacts no service. It works best with the superpowers plugin installed, which provides the skills it suggests. thinkfirst-skillcue is a separate project by Victor Cheong, not part of superpowers.
Source 2 files
hooks/register.tsx 171 lines
1import { 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}
171
types/index.d.ts 11 lines
1export 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