Adds one line to a prompt that clearly matches a skill (review, plan-first, writing, ahpcc, assess-plugin, agents-md-scaffold), so the skill is used when the…

Install: /plugin install skill-router --marketplace samuelbfernandes/moddeck
Makes sure a skill is used when the task calls for it. A prompt that clearly asks for one of the toolkit's skills gets one added line naming it; the model then loads the skill with the Skill tool.
Local keyword rules, first match wins, no model call:
| Skill | A prompt that mentions |
|---|---|
| ahpcc | ahpcc, pinnacle, slurm, sbatch, squeue, "on the cluster" |
| assess-plugin | assess / vet / "safe to install" with plugin, mod, mcp or skill |
| agents-md-scaffold | AGENTS.md, or starting a new repo with rules |
| review | review, second opinion, "check this", "before I commit/push" |
| plan-first | a long request to build an app, portal, pipeline or package; "plan/scope/align first" |
| writing | email, abstract, cover letter, rewrite, humanize, "sounds like AI", "draft a..." |
Each skill is hinted once per session. Slash commands, > prompts, replies under five words, and prompts that already say "the <skill> skill" get nothing. About 30 tokens on a matching prompt.
/skill-router status: skills hinted this session /skill-router on | off
Keywords are a guess at intent: a hint is advice ("unless the request clearly is not that"), not a command. The skills' own descriptions still let the model choose them without a hint.
skill-router → isOff, hinted (see types/index.d.ts).
hooks/register.tsx 58 lines1// skill-router: a prompt that clearly matches one of the toolkit's skills gets one added line telling the model which
2// skill to load. Local keyword rules (no model call); each skill is hinted once per session; slash commands, short
3// replies and prompts that already name the skill get nothing. /skill-router on|off|status.
4// FAIL OPEN: any error sends the prompt unchanged.
5import { atom, read, update } from 'claude-code'
6import type { EngineInterface, Register } from 'claude-code'
7
8import { hintFor, pick } from './lib'
9
10type Dollar = EngineInterface
11
12const COMMAND = 'skill-router'
13
14const isOff = atom({ plugin: 'skill-router', key: 'isOff' } as const, false)
15const hinted = atom({ plugin: 'skill-router', key: 'hinted' } as const, [])
16
17async function run($: Dollar, args: string): Promise<string> {
18 const word = args.trim().toLowerCase()
19 if (word === 'on' || word === 'off') {
20 await $.store.set('off', word === 'off')
21 await update($, isOff, () => word === 'off')
22 return `skill-router · ${word}`
23 }
24 if (word === '' || word === 'status') {
25 const list = await read($, hinted)
26 return `skill-router · ${(await read($, isOff)) ? 'off' : 'on'} · hinted this session: ${list.length === 0 ? 'none' : list.join(', ')}`
27 }
28 return 'skill-router · use /skill-router [on|off|status]; nothing changed'
29}
30
31export const register: Register = on => {
32 on('session.start', async ($, e, next) => {
33 try {
34 const off = (await $.store.get('off')) === true
35 await update($, isOff, () => off)
36 await update($, hinted, () => [])
37 } catch {
38 // reads as on
39 }
40 try {
41 await $.command.register({ name: COMMAND, description: 'Hints the matching skill on a prompt that clearly asks for it: /skill-router [on|off|status]', argumentHint: '[on|off|status]', immediate: true })
42 } catch {
43 // fail-open: the mod still works without its command
44 }
45 return next(e)
46 })
47
48 on('command.run', { command: COMMAND }, async ($, e) => ({ text: await run($, e.args) })).catch(() => ({ text: 'skill-router · that did not work, nothing changed' }))
49
50 on('prompt.submit', async ($, e, next) => {
51 if ((await read($, isOff)) || e.origin?.kind !== 'user') return next(e)
52 const skill = pick(e.text, await read($, hinted))
53 if (skill === undefined) return next(e)
54 await update($, hinted, list => [...list, skill])
55 return next({ ...e, context: [...(e.context ?? []), hintFor(skill)] })
56 }).catch(($, e, next) => next(e))
57}
58hooks/lib.ts 30 lines1// Pure helpers for skill-router: which skill a prompt clearly asks for. Local keyword rules, first match wins.
2
3export type Rule = { skill: string; test: (text: string, words: number) => boolean }
4
5export const RULES: Rule[] = [
6 { skill: 'ahpcc', test: t => /\b(ahpcc|pinnacle|slurm|sbatch|squeue|on the (hpc|cluster)|hpc cluster)\b/i.test(t) },
7 { skill: 'assess-plugin', test: t => /\b(assess|vet|audit|is it safe|worth installing|safe to install|should i install)\b[^.?!]*\b(plugin|mod|mcp|skill)s?\b/i.test(t) },
8 { skill: 'agents-md-scaffold', test: t => /\bagents\.md\b|\b(bootstrap|scaffold|set ?up|start|create)\b[^.?!]*\b(new )?(repo|repository|project)\b/i.test(t) && /\b(rules|agents|guard|new)\b/i.test(t) },
9 { skill: 'review', test: t => /\b(review|second opinion|theory review|check (this|my|the)|look (this|it) over|before (i |we )?(commit|push))\b/i.test(t) },
10 { skill: 'plan-first', test: (t, n) => (n >= 12 && /\b(build|implement|create|design|develop|write)\b[^.?!]{0,60}\b(app|portal|pipeline|package|dashboard|tool|feature|workflow|simulation|analysis|framework)\b/i.test(t)) || /\b(plan|scope|align|spec)\b[^.?!]*\b(first|before)\b/i.test(t) },
11 { skill: 'writing', test: t => /\b(email|e-mail|abstract|cover letter|tagline|rewrite|reword|humani[sz]e|de-?slop|sounds? like (ai|chatgpt)|draft (a|an|the|my))\b/i.test(t) },
12]
13
14const wordCount = (s: string): number => s.trim().split(/\s+/).filter(Boolean).length
15
16/** The skill to hint at: none for a slash command, a short reply, or a prompt that already names the skill. */
17export function pick(text: string, already: readonly string[]): string | undefined {
18 const t = text.trim()
19 const n = wordCount(t)
20 if (t.startsWith('/') || t.startsWith('>') || n < 5) return undefined
21 for (const r of RULES) {
22 if (already.includes(r.skill) || new RegExp(`\\b${r.skill}\\s+skill\\b|\\bskill\\s+${r.skill}\\b`, 'i').test(t)) continue
23 if (r.test(t, n)) return r.skill
24 }
25 return undefined
26}
27
28export const hintFor = (skill: string): string =>
29 `skill-router: this looks like a job for the ${skill} skill. Load it with the Skill tool and follow it, unless the request clearly is not that.`
30types/index.d.ts 11 lines1declare module 'claude-code' {
2 interface PluginState {
3 'skill-router': {
4 /** True while switched off (it ships on). */
5 isOff: boolean
6 /** Skills hinted this session, in order. */
7 hinted: string[]
8 }
9 }
10}
11