Reusable prompt fragments kept across sessions: /snip add|rm|list|show, and ;;name expands in prompts

Reusable prompt fragments, kept across sessions and expanded inline with ;;name.
;;name, replaces each token with the snippet text before Claude sees it.;;name glued to a preceding letter, digit, _, - or ; is left alone. Unknown names are left as typed and listed in a toast.a-z, 0-9, _ and -. Prompts starting with /snip are not expanded.| Command | Effect |
|---|---|
/snip add <name> <text> | Save (or overwrite) a snippet |
/snip rm <name> | Remove a snippet |
/snip list | List snippet names (default when no argument) |
/snip show <name> | Print a snippet's text |
Example: /snip add review Review this diff for bugs and missing tests. then type ;;review in a prompt.
claude --plugin-dir /path/to/ModsTools/mods/prompt-snippets
(no prompt) elsewhere; snippets expand only in typed prompts.;;other is inserted as is.claude plugin validate mods/prompt-snippets
claude plugin test mods/prompt-snippets # 10 testshooks/register.ts 58 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import { expand, parseCommand } from './snippets'
4import type { Snippets } from './snippets'
5
6const KEY = 'snippets'
7
8async function load($: EngineInterface): Promise<Snippets> {
9 const value = await $.store.get(KEY)
10
11 return typeof value === 'object' && value !== null ? (value as Snippets) : {}
12}
13
14export const register: Register = on => {
15 on('session.start', async ($, e, next) => {
16 await $.command.register({
17 name: 'snip',
18 description: 'Prompt snippets: add <name> <text> | rm <name> | list | show <name>; use ;;name in a prompt',
19 })
20
21 return next(e)
22 })
23
24 on('command.run', { command: 'snip' }, async ($, e) => {
25 const cmd = parseCommand(e.args)
26 if (cmd.action === 'error') return { text: cmd.message }
27 const snippets = await load($)
28 if (cmd.action === 'list') {
29 const names = Object.keys(snippets).sort()
30
31 return { text: names.length === 0 ? 'No snippets yet.' : `Snippets: ${names.map(n => `;;${n}`).join(' ')}` }
32 }
33 if (cmd.action === 'show') {
34 const body = Object.hasOwn(snippets, cmd.name) ? snippets[cmd.name] : undefined
35
36 return { text: body === undefined ? `No snippet "${cmd.name}".` : `;;${cmd.name} = ${body}` }
37 }
38 if (cmd.action === 'rm') {
39 if (!Object.hasOwn(snippets, cmd.name)) return { text: `No snippet "${cmd.name}".` }
40 const { [cmd.name]: _removed, ...rest } = snippets
41 await $.store.set(KEY, rest)
42
43 return { text: `Removed ;;${cmd.name}.` }
44 }
45 await $.store.set(KEY, { ...snippets, [cmd.name]: cmd.text })
46
47 return { text: `Saved ;;${cmd.name}.` }
48 })
49
50 on('prompt.submit', async ($, e, next) => {
51 if (!e.text.includes(';;') || e.text.trimStart().startsWith('/snip')) return next(e)
52 const { text, unknown } = expand(e.text, await load($))
53 if (unknown.length > 0) $.ui.toast(`Unknown snippet${unknown.length === 1 ? '' : 's'}: ${unknown.map(n => `;;${n}`).join(', ')}`)
54
55 return next({ ...e, text })
56 })
57}
58hooks/snippets.ts 48 lines1export type Snippets = Record<string, string>
2
3const NAME = /^[a-z0-9_-]+$/
4const TOKEN = /;;([a-z0-9_-]+)(?![A-Za-z0-9_-])/g
5
6export const isValidName = (name: string) => NAME.test(name)
7
8export type Command =
9 | { action: 'add'; name: string; text: string }
10 | { action: 'rm'; name: string }
11 | { action: 'show'; name: string }
12 | { action: 'list' }
13 | { action: 'error'; message: string }
14
15export function parseCommand(args: string): Command {
16 const m = /^\s*(\S+)(?:\s+(\S+))?(?:\s+([\s\S]*))?$/.exec(args)
17 const action = m?.[1] ?? 'list'
18 const name = m?.[2] ?? ''
19 const text = (m?.[3] ?? '').trim()
20 if (action === 'list') return { action: 'list' }
21 if (action !== 'add' && action !== 'rm' && action !== 'show') {
22 return { action: 'error', message: 'Usage: /snip add <name> <text> | rm <name> | list | show <name>' }
23 }
24 if (!isValidName(name)) return { action: 'error', message: `Invalid name "${name}": use a-z, 0-9, _ and -.` }
25 if (action === 'add') {
26 return text === '' ? { action: 'error', message: 'Usage: /snip add <name> <text>' } : { action, name, text }
27 }
28
29 return { action, name } as Command
30}
31
32export function expand(text: string, snippets: Snippets): { text: string; unknown: string[] } {
33 const unknown: string[] = []
34 const out = text.replace(TOKEN, (token, name: string, offset: number) => {
35 const before = offset === 0 ? '' : text.charAt(offset - 1)
36 if (/[A-Za-z0-9_;-]/.test(before)) return token
37 const body = Object.hasOwn(snippets, name) ? snippets[name] : undefined
38 if (body === undefined) {
39 if (!unknown.includes(name)) unknown.push(name)
40 return token
41 }
42
43 return body
44 })
45
46 return { text: out, unknown }
47}
48