Save a prompt once, insert it anywhere with /snip, and quote your selection with /quote.

Save a prompt once, insert it anywhere with /snip, and quote your selection with /quote.
Each command answers in one row under the command:
> /snip save review
Saved snippet review with your last prompt. /snip review inserts it.
> /snip list
| Name | Snippet |
| ---------- | ------------------------------------------------------------- |
| commit-msg | Write a conventional commit message for the staged changes.… |
| explain | Explain what this function does, step by step. |
| review | Review the staged diff for bugs, missing tests and unclear n… |
> /snip commit-msg
Inserted snippet commit-msg.
> /quote
Quoted the selection into the prompt.
/snip list answer is drawn as Markdown, so the table renders as a table in the terminal and in the desktop app. Every other answer is Claude Code's own row.claude -p), /snip <name> and /quote leave the box alone and answer the text to copy:> /snip commit-msg
This session has no prompt box a mod can fill (the desktop app, claude -p). Copy it from here:
Write a conventional commit message for the staged changes. Keep the subject under 72 characters.
| Command | What it does |
|---|---|
/snip save <name> | Saves the last prompt you sent. Saving under an existing name replaces it. |
/snip <name> | Inserts the snippet into the terminal's prompt box at the cursor. |
/snip list | Answers a table of your snippets: each name and its first 60 characters. |
/snip rm <name> | Removes a snippet. |
/quote | Puts your current mouse selection into the prompt as > lines, followed by a blank line. |
/snip save takes the last prompt you sent from the terminal, the desktop app or Remote Control. Slash commands, reminders and messages from other sessions, agents or channels do not count./snip save reads the transcript instead: the last user message that is not a command line, a tool result, a reminder, the summary /compact leaves, or a teammate or channel message.a-z, 0-9 and -. save, list, rm and help are taken by the subcommands.No snippet named x. /snip list shows yours./plugin install prompt-snippets --marketplace KROU4/claude-code-mod-showroomclaude plugin install prompt-snippets --marketplace KROU4/claude-code-mod-showroomKROU4/claude-code-mod-showroom → Add pluginclaude -p has none./quote reads the mouse selection in the desktop app and in the terminal's fullscreen layout. The terminal's main screen never shows a mod its selection, and /quote says so there.MIT. See LICENSE.
hooks/register.tsx 184 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, PromptFillResult, PromptOrigin, Register } from 'claude-code'
3
4const NAME = /^[a-z0-9-]{1,32}$/
5const VERBS = new Set(['save', 'list', 'rm', 'help'])
6const PREVIEW = 60
7// Where a prompt the person wrote arrives from: the terminal, the desktop app or another SDK host, Remote Control.
8const BY_PERSON = new Set<PromptOrigin['kind']>(['composer', 'sdk', 'bridge'])
9// User-role rows the person did not type: a command's record and output, reminders and
10// notices, the summary /compact leaves, messages from teammates and channels.
11const NOT_TYPED =
12 /^(<(command-|local-command-|system-reminder|bash-|task-notification|teammate-message|channel)|\[Request interrupted|This session is being continued from a previous conversation)/
13
14const typed = atom({ plugin: 'prompt-snippets', key: 'typed' } as const, null)
15
16const USAGE = [
17 '- `/snip save <name>` saves your last prompt',
18 '- `/snip <name>` inserts a snippet into the prompt',
19 '- `/snip list` shows your snippets',
20 '- `/snip rm <name>` removes one',
21].join('\n')
22
23// The answer is drawn as Markdown: keep its punctuation from acting.
24const escape = (text: string) => text.replace(/[\\`*_~<>[\]|&]/g, '\\$&')
25
26const missing = (name: string) => `No snippet named ${name}. \`/snip list\` shows yours.`
27const badName = (name: string) =>
28 `${escape(name)} is not a snippet name: use 1-32 characters of \`a-z\`, \`0-9\` and \`-\`.`
29const failed = (command: string) => (error: unknown) =>
30 `\`${command}\` did not finish: ${error instanceof Error ? error.message : String(error)}`
31
32// A fence longer than any backtick run in the text, so the text shows as it is.
33function fence(text: string): string {
34 const runs = text.match(/`+/g) ?? []
35 const bar = '`'.repeat(Math.max(3, ...runs.map(run => run.length + 1)))
36
37 return `${bar}\n${text.trimEnd()}\n${bar}`
38}
39
40// Puts text in at the cursor and answers `done`, or why it could not.
41async function insert($: EngineInterface, text: string, done: string): Promise<string> {
42 // Only the terminal's prompt box takes a mod's text: the desktop app draws its own, -p has none.
43 const hasBox = (await $.session.surfaces()).includes('terminal')
44 const filled: PromptFillResult = hasBox
45 ? await $.prompt.fill({ text, mode: 'insert' })
46 : { isFilled: false, refusal: 'no_composer' }
47
48 if (filled.isFilled) return done
49 if (filled.refusal === 'no_composer') {
50 return `This session has no prompt box a mod can fill (the desktop app, \`claude -p\`). Copy it from here:\n\n${fence(text)}`
51 }
52 if (filled.refusal === 'dialog') return 'A dialog holds the keys. Close it, then run the command again.'
53
54 return 'Another plugin kept the text out of the prompt box.'
55}
56
57async function lastPrompt($: EngineInterface): Promise<string | undefined> {
58 const sent = await read($, typed)
59 if (sent !== null) return sent
60
61 // Nothing sent since the mod loaded (a resumed session): the transcript's last row that reads as typed.
62 const rows = await $.session.messages()
63
64 return rows.findLast(
65 row =>
66 row.role === 'user' &&
67 !row.toolResults?.length &&
68 row.text.trim() !== '' &&
69 !NOT_TYPED.test(row.text.trimStart()),
70 )?.text
71}
72
73function preview(text: string): string {
74 const flat = text.replace(/\s+/g, ' ').trim()
75
76 return escape(flat.length > PREVIEW ? `${flat.slice(0, PREVIEW).trimEnd()}…` : flat)
77}
78
79async function table($: EngineInterface): Promise<string> {
80 const names = (await $.store.keys()).sort()
81 if (names.length === 0) {
82 return 'No snippets yet. Send a prompt, then `/snip save <name>` keeps it.'
83 }
84 const rows = await Promise.all(
85 names.map(async name => `| ${name} | ${preview(String(await $.store.get(name)))} |`),
86 )
87
88 return ['| Name | Snippet |', '| --- | --- |', ...rows].join('\n')
89}
90
91async function snip($: EngineInterface, args: string): Promise<string> {
92 const [verb = '', name = '', ...rest] = args.trim().split(/\s+/)
93
94 if (verb === '' || verb === 'help') return USAGE
95 if (verb === 'list') return name === '' ? table($) : `\`/snip list\` takes no name.\n\n${USAGE}`
96
97 if (verb === 'save' || verb === 'rm') {
98 if (name === '') return `Name the snippet: \`/snip ${verb} <name>\`.`
99 if (!NAME.test(name) || rest.length > 0) return badName([name, ...rest].join(' '))
100 if (VERBS.has(name)) return `${name} is a \`/snip\` command, so it cannot name a snippet.`
101 const held = await $.store.get(name)
102
103 if (verb === 'rm') {
104 if (held === undefined) return missing(name)
105 await $.store.delete(name)
106 return `Removed snippet ${name}.`
107 }
108
109 const prompt = await lastPrompt($)
110 if (prompt === undefined) return 'No prompt to save yet. Send one, then `/snip save <name>`.'
111 try {
112 await $.store.set(name, prompt)
113 } catch {
114 return 'The snippet store refused it: all snippets together hold at most 4 MiB. `/snip rm <name>` frees room.'
115 }
116 return `${held === undefined ? 'Saved' : 'Replaced'} snippet ${name} with your last prompt. \`/snip ${name}\` inserts it.`
117 }
118
119 if (!NAME.test(verb) || name !== '') return `${badName(args.trim())}\n\n${USAGE}`
120 const text = await $.store.get(verb)
121 if (typeof text !== 'string') return missing(verb)
122
123 return insert($, text, `Inserted snippet ${verb}.`)
124}
125
126async function quote($: EngineInterface, isFullscreen: boolean): Promise<string> {
127 const lines = (await $.ui.selection())?.text.replace(/\s+$/, '')
128 if (!lines) {
129 // The main-screen terminal never shows a mod its selection.
130 return isFullscreen
131 ? 'Nothing is selected. Select text with the mouse, then `/quote`.'
132 : "Nothing is selected that `/quote` can read: it reads the mouse selection in the desktop app and in the terminal's fullscreen layout."
133 }
134 const quoted = `${lines
135 .split(/\r?\n/)
136 .map(line => `> ${line}`)
137 .join('\n')}\n\n`
138
139 return insert($, quoted, 'Quoted the selection into the prompt.')
140}
141
142export const register: Register = on => {
143 on('session.start', async ($, e, next) => {
144 await $.command.register({
145 name: 'snip',
146 description: 'Save, list and insert reusable prompt snippets',
147 argumentHint: '<name> | save <name> | list | rm <name>',
148 immediate: true,
149 })
150 await $.command.register({
151 name: 'quote',
152 description: 'Quote your mouse selection into the prompt',
153 immediate: true,
154 })
155
156 return next(e)
157 })
158
159 // Only notes the prompt down: a failure lets it through.
160 on('prompt.submit', async ($, e, next) => {
161 if (BY_PERSON.has(e.origin.kind) && !e.isMeta && !e.command && e.text.trim() !== '') {
162 await update($, typed, () => e.text)
163 }
164
165 return next(e)
166 }).catch(($, e, next) => next(e))
167
168 on('command.run', { command: 'snip' }, async ($, e) => ({
169 text: await snip($, e.args).catch(failed('/snip')),
170 }))
171
172 on('command.run', { command: 'quote' }, async ($, e) => ({
173 text: await quote($, e.presentation.isFullscreen).catch(failed('/quote')),
174 }))
175
176 // The list is a Markdown table: draw it as one rather than as the row's plain text.
177 on('ui.render', { component: 'CommandOutput', props: { command: 'snip' } }, ($, e, next) => {
178 if (e.props.isErrored || e.props.args.trim() !== 'list') return next(e)
179 const { Markdown } = $.ui.resolve(e)
180
181 return <Markdown text={e.props.text} />
182 })
183}
184types/index.d.ts 9 lines1// The last prompt the person sent this session; null until they send one.
2export type LastPrompt = string | null
3
4declare module 'claude-code' {
5 interface PluginState {
6 'prompt-snippets': { typed: LastPrompt }
7 }
8}
9