Expand #hashtag snippets in the prompt, with # autocomplete

A Claude Code mod that expands #hashtag snippets in the prompt.
# and a prefix: matching snippets show in the typeahead. Press TAB to replace the #word with the snippet text.#tag still in the message is expanded when you press Enter. Unknown tags stay as typed.#tags, and <prepend>…</prepend> / <append>…</append> move text to the start or end of the message.Requires Claude Code v2.1.302 or later (tested on 2.1.302).
Install: see the marketplace README.
Edit snippets in the SNIPPETS array in hooks/register.ts.
hooks/register.ts 240 lines1/**
2 * Snippets - expand #hashtag snippets in user input, with #-autocomplete.
3 * Port of ~/.pi/agent/extensions/snippets.ts. Edit SNIPPETS; the mod hot-reloads.
4 */
5
6import type { Register } from 'claude-code'
7
8interface Snippet {
9 name: string;
10 aliases?: string[];
11 description?: string;
12 content: string;
13}
14
15const SNIPPETS: Snippet[] = [
16 {
17 name: "uv",
18 aliases: ["uv"],
19 description: "It provides uv information",
20 content: "you could use uv with VIRTUAL_ENV=/home/$USER/.venvs/uness uv run --sync [SCRIPTNAME]",
21 },
22 {
23 name: "premortem",
24 aliases: ["premortem"],
25 description: "premortem analysis",
26 content: "perform an epistemic premortem analysis using formal counterfactual reasoning to consider the consequences of following your suggestion.",
27 },
28 {
29 name: "blogpost",
30 aliases: ["blogpost"],
31 description: "write a medium-style blogspot",
32 content: "write an article in clear prose for the Medium.com platform. Avoid telegraphic sentences. The text must be self-contained. AI-sounding sentences and wording removed",
33 },
34 {
35 name: "datastorytelling",
36 aliases: ["datastorytelling"],
37 description: "write a datastorytelling",
38 content: "provide a data storytelling walkthrough as if it were a manual processing task, include intermediate inputs/outputs",
39 },
40 {
41 name: "defensible",
42 aliases: ["defensible"],
43 description: "Recover as much of the text's quality as possible ",
44 content: "select the most defensible cases and develop the strongest possible argument for each one",
45 },
46 {
47 name: "evidence",
48 aliases: ["evidence"],
49 description: "Show me raw evidence",
50 content: "show me the raw evidence for each of your claims. Identify any assumptions you have made and explain what you cannot verify",
51 },
52 {
53 name: "falsification",
54 aliases: ["epistemic"],
55 description: "epistemic falsification",
56 content: "epistemically audit your answer: (1) Fix definitions strictly to avoid semantic ambiguity; (2) State the core claim alongside its necessary auxiliary assumptions; (3) Define the operational domain where it holds; (4) Identify the specific empirical or systemic conditions under which the core claim is definitively falsified.",
57 },
58 {
59 name: "tldr",
60 aliases: ["tldr"],
61 description: "tl;dr",
62 content: "provide a TL;DR summary of your answer in plain text for a project manager.",
63 },
64 {
65 name: "cognitive_bridge",
66 aliases: ["cognitive_bridge"],
67 description: "cognitive bridge",
68 content: "what cognitive or epistemic bridge would a senior scientist use to spot the problem between X and Y?",
69 },
70 {
71 name: "cognitive_bias",
72 aliases: ["cognitive_bias"],
73 description: "cognitive bias",
74 content: "identify the cognitive biases that influenced your response and refine its epistemological quality.",
75 },
76 {
77 name: "dsl",
78 aliases: ["dsl"],
79 description: "dsl",
80 content: "show me the latent DSL (Domain-Specific language) for this text",
81 },
82 {
83 name: "traceability",
84 aliases: ["traceability"],
85 description: "traceability",
86 content: "follow every claim back to the measurement that produced it, and treat whatever resists tracing as the signal itself",
87 },
88 {
89 name: "claude",
90 aliases: ["claude"],
91 description: "claude code",
92 content: "use command ```claude -p \"[TASK]\" ---allowed-tools Read,Write``` to ask for a second opinion for solving the problem",
93 },
94 {
95 name: "codex",
96 aliases: ["codex"],
97 description: "codex",
98 content: "use command ```codex exec --skip-git-repo-check \"[TASK]\"``` to ask for a second opinion for solving the problem",
99 },
100 {
101 name: "tmux_pane",
102 aliases: ["tmux_pane"],
103 description: "launch an operation in a tmux pane",
104 content: "use another tmux pane to evaluate the output: a) tmux send-keys -t [SESSION:WINDOW.PANE] \"pi --provider litellm --model openai/Inferact/Qwen3.8-27B-NVFP4 --thinking off -p \"[TASK]\" Enter , b) tmux capture-pane -p -t [SESSION:WINDOW.PANE] -S -[SCROLLBALL_LINES]",
105 },
106 {
107 name: "pi-link",
108 aliases: ["pi-link"],
109 description: "launch an operation in a tmux pane",
110 content: "use tmux send-keys -t [SESSION:WINDOW.PANE] \"pi --link-name slave\" Enter, to start a new pi agent and next, use link_send to ask to \"slave\" to perform this [TASK]",
111 },
112 {
113 name: "dead_code",
114 aliases: ["dead-code"],
115 description: "dead-code",
116 content: "remove associated dead code, leftovers, orphan code and landmines",
117 },
118 {
119 name: "confirm",
120 aliases: ["confirm"],
121 description: "Confirm understanding",
122 content: "confirm you understand, no coding yet. Wait for further instructions.",
123 },
124 {
125 name: "playwright",
126 aliases: ["playwright"],
127 description: "Use playwright headed vnc skill",
128 content: "use the skill playwright-headed-vnc",
129 },
130 {
131 name: "spurious_rigor",
132 aliases: ["spurious_rigor"],
133 description: "spurious_rigor",
134 content: "detect outcome biases like unfaithful Post-Hoc Rationalization, Spurious Rigor / Mathiness, Cargo Cult Reasoning, Framework-Forcing / Sycophancy and Formal Confabulation, Automation bias.",
135 }
136];
137
138const MAX_EXPANSIONS = 25;
139const HASHTAG = /#([a-z0-9\-_]+)/gi;
140
141const registry = new Map<string, Snippet>();
142for (const s of SNIPPETS) {
143 registry.set(s.name.toLowerCase(), s);
144 for (const a of s.aliases ?? []) registry.set(a.toLowerCase(), s);
145}
146
147interface Expanded {
148 text: string;
149 prepend: string[];
150 append: string[];
151}
152
153function extractBlocks(content: string): { inline: string; prepend: string[]; append: string[] } {
154 const prepend: string[] = [];
155 const append: string[] = [];
156 // Unclosed block runs to the end of the content; a stray closing tag stays inline.
157 const inline = content.replace(/<(prepend|append)>([\s\S]*?)(?:<\/\1>|$)/gi, (_m, tag: string, body: string) => {
158 const trimmed = body.trim();
159 if (trimmed) (tag.toLowerCase() === "prepend" ? prepend : append).push(trimmed);
160 return "";
161 });
162 return { inline: inline.trim(), prepend, append };
163}
164
165export function expand(text: string, counts: Map<string, number>): Expanded {
166 const prepend: string[] = [];
167 const append: string[] = [];
168 let out = text;
169 let previous: string;
170 let loop = false;
171
172 do {
173 previous = out;
174 out = out.replace(HASHTAG, (match, name: string) => {
175 const key = name.toLowerCase();
176 const snippet = registry.get(key);
177 if (!snippet) return match;
178
179 const count = (counts.get(key) ?? 0) + 1;
180 if (count > MAX_EXPANSIONS) {
181 loop = true;
182 return match;
183 }
184 counts.set(key, count);
185
186 const blocks = extractBlocks(snippet.content);
187 for (const [list, dest] of [
188 [blocks.prepend, prepend],
189 [blocks.append, append],
190 ] as const) {
191 for (const block of list) {
192 const r = expand(block, counts);
193 dest.push(r.text);
194 prepend.push(...r.prepend);
195 append.push(...r.append);
196 }
197 }
198 const nested = expand(blocks.inline, counts);
199 prepend.push(...nested.prepend);
200 append.push(...nested.append);
201 return nested.text;
202 });
203 } while (out !== previous && !loop);
204
205 return { text: out, prepend, append };
206}
207
208export function expandAll(text: string): string {
209 const r = expand(text, new Map())
210 return [r.prepend.join('\n\n'), r.text.trim() ? r.text : '', r.append.join('\n\n')].filter(Boolean).join('\n\n')
211}
212
213export const register: Register = (on) => {
214 on('prompt.autocomplete', { token: /^#/ }, async ($, e, next) => {
215 const query = e.token.slice(1).toLowerCase()
216 if (!/^[a-z0-9_-]*$/.test(query)) return next(e)
217 const mine = SNIPPETS.filter(
218 (s) => !query || [s.name, ...(s.aliases ?? [])].some((n) => n.toLowerCase().includes(query)),
219 )
220 .sort((a, b) => a.name.localeCompare(b.name))
221 .map((s) => {
222 const aliases = (s.aliases ?? []).filter((a) => a !== s.name).map((a) => `#${a}`).join(', ')
223 const desc = s.description ?? (s.content.split("\n")[0] ?? "").slice(0, 60)
224 return {
225 text: expandAll(`#${s.name}`) || `#${s.name}`,
226 label: `#${s.name}`,
227 description: aliases ? `${desc} [${aliases}]` : desc,
228 }
229 })
230 const { suggestions } = await next(e)
231 return { suggestions: [...suggestions, ...mine] }
232 })
233
234 on('prompt.submit', async ($, e, next) => {
235 if (e.command || !e.text) return next(e)
236 const text = expandAll(e.text)
237 return text !== e.text ? next({ ...e, text }) : next(e)
238 })
239}
240