Put ++ at the start or end of a prompt to have it rewritten into a clear prompt for review before sending

Three small mods for Claude Code. Install the ones you want.
claude plugin marketplace add GururOzatmaca/claude-mods
claude plugin install prompt-boost@claude-mods
claude plugin install reply-tools@claude-mods
claude plugin install jump-list@claude-mods
Needs Claude Code 2.1.287 or newer. If the install says options not yet set, ignore it; the defaults work.
Add ++ to a messy prompt. It gets rewritten in clear English and put back in the box. You check it, then press Enter.

++ at the start or end. ++o uses Opus, ++q adds open questions./ap undo brings back your original.A copy button on every code block, and a tl;dr button under long replies.

⧉ copy all copies several shell commands at once.tl;dr keeps every step and warning. ↑ original switches back./plugin configure reply-tools@claude-mods.A side list of every prompt in the session, with its time. Click one to jump back.
<img src="docs/images/jump-list-pane.png" alt="Jump list" width="232">
↓ end goes to the bottom. – folds it into one line./jumps.Mods run with your permissions. prompt-boost and tl;dr make one model call each time you use them. jump-list reads the session's transcript file. Run claude plugin validate <folder> to see exactly what a mod does.
MIT license.
hooks/register.tsx 137 lines1import type { EngineInterface, Register, Timer } from 'claude-code'
2
3import { ORANGE } from './rainbow'
4import { buildPrompt, parseTrigger, systemFor, triggerSpan } from './rules'
5
6const TIMEOUT_MS = 30000
7const MAX_TOKENS = 600
8const BANNER = 'Prompt boosted!'
9const NOTICE_MARKER = 'prompt-boost:boosted'
10const DOT_MS = 400
11
12let boosting: { model: string; dots: number; timer: Timer } | undefined
13
14function startBoosting($: EngineInterface, model: string) {
15 boosting?.timer.cancel()
16 const timer = $.clock.every(DOT_MS, () => {
17 if (boosting === undefined) return
18 boosting.dots = (boosting.dots + 1) % 4
19 $.ui.invalidate('ui.render')
20 })
21 boosting = { model, dots: 0, timer }
22 $.ui.invalidate('ui.render')
23}
24
25function stopBoosting($: EngineInterface) {
26 boosting?.timer.cancel()
27 boosting = undefined
28 $.ui.invalidate('ui.render')
29}
30
31export const register: Register = on => {
32 let last: { original: string; rewrite: string; model: string; seconds: number } | undefined
33
34 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
35 if (boosting === undefined || e.props.hasSurvey) return next(e)
36 const { Text } = $.ui.resolve(e)
37 return (
38 <Text color={ORANGE}>
39 Boosting prompt{'.'.repeat(boosting.dots)}
40 <Text dimColor> ({boosting.model})</Text>
41 </Text>
42 )
43 })
44
45 on('prompt.edit', async ($, e, next) => {
46 const box = await next(e)
47 const span = triggerSpan(box.text)
48 if (span === undefined) return box
49 return {
50 ...box,
51 decorations: [...(box.decorations ?? []), { start: span[0], end: span[1], color: ORANGE }],
52 }
53 })
54
55 on('session.append', async ($, e, next) => {
56 if (e.door !== 'notice') return next(e)
57 const isBoostNotice = e.message.content.some(
58 block => block.type === 'text' && String(block.text).includes(NOTICE_MARKER),
59 )
60 if (!isBoostNotice) return next(e)
61 return next({ ...e, message: { ...e.message, content: [{ type: 'text', text: BANNER }] } })
62 }).catch(($, e, next) => next(e))
63
64 on('session.start', async ($, e, next) => {
65 await $.command.register({
66 name: 'ap',
67 description: 'prompt-boost: show the last rewrite; "undo" puts the original back in the prompt box',
68 argumentHint: '[undo]',
69 immediate: true,
70 })
71 return next(e)
72 })
73
74 on('command.run', { command: 'ap' }, async ($, e) => {
75 if (last === undefined) {
76 return { text: 'No rewrite yet. Prefixes: ++ (clean wording, Haiku), ++o (clean, Opus), ++q (structured + questions), ++oq (Opus + questions).' }
77 }
78 if (String(e.args ?? '').trim() === 'undo') {
79 await $.prompt.fill({ text: last.original })
80 return { text: 'Original prompt restored to the box.' }
81 }
82 return {
83 text: `Original:\n${last.original}\n\nRewrite (${last.model}, ${last.seconds}s):\n${last.rewrite}`,
84 }
85 })
86
87 on('prompt.submit', async ($, e, next) => {
88 if (e.origin.kind !== 'composer') return next(e)
89 const trigger = parseTrigger(e.text)
90 if (trigger === undefined) return next(e)
91
92 const started = await $.clock.now()
93 startBoosting($, trigger.model)
94 const history = (await $.session.messages()).map(m => ({ role: m.role, text: m.text }))
95 const result = await $.model.complete({
96 model: trigger.model,
97 system: systemFor(trigger.mode),
98 prompt: buildPrompt(trigger.raw, history),
99 maxTokens: MAX_TOKENS,
100 timeoutMs: TIMEOUT_MS,
101 })
102 stopBoosting($)
103 const seconds = Math.round(((await $.clock.now()) - started) / 100) / 10
104 const hasAttachments = (e.attachments?.length ?? 0) > 0
105
106 if (!result.isAnswered || result.text.trim() === '') {
107 const reason = result.isAnswered ? 'empty reply' : result.reason
108 if (hasAttachments) {
109 $.ui.toast(`Boost failed (${reason}); sent the original with its images`)
110 return next({ ...e, text: trigger.raw })
111 }
112 await $.prompt.fill({ text: trigger.raw })
113 return { drop: `prompt-boost: rewrite failed (${reason}). Original restored to the prompt box.` }
114 }
115
116 const rewrite = result.text.trim()
117 last = { original: trigger.raw, rewrite, model: trigger.model, seconds }
118 if (hasAttachments) {
119 $.ui.toast('Boosted and sent with images. /ap shows the rewrite')
120 return next({ ...e, text: rewrite })
121 }
122 await $.prompt.fill({ text: rewrite })
123
124 return { drop: NOTICE_MARKER }
125 }).catch(async ($, e, next) => {
126 const trigger = parseTrigger(e.text)
127 if (trigger === undefined) return next(e)
128 stopBoosting($)
129 if ((e.attachments?.length ?? 0) > 0) {
130 $.ui.toast('Boost errored; sent the original with its images')
131 return next({ ...e, text: trigger.raw })
132 }
133 await $.prompt.fill({ text: trigger.raw })
134 return { drop: 'prompt-boost: rewrite errored. Original restored to the prompt box.' }
135 })
136}
137hooks/rainbow.ts 2 lines1export const ORANGE = '#ff8c00'
2hooks/rules.ts 98 lines1export type Mode = 'clean' | 'ask'
2
3export type Trigger = { model: 'haiku' | 'opus'; mode: Mode; raw: string }
4
5const PREFIX = /^\+\+(o|q|oq|qo)?(?:\s+|$)/
6const SUFFIX = /(?:^|\s)\+\+(o|q|oq|qo)?$/
7
8type Found = { start: number; end: number; flags: string; raw: string }
9
10function find(text: string): Found | undefined {
11 const lead = text.length - text.trimStart().length
12 const body = text.trim()
13 const prefix = PREFIX.exec(body)
14 if (prefix !== null) {
15 const marker = prefix[0].trimEnd()
16 return { start: lead, end: lead + marker.length, flags: prefix[1] ?? '', raw: body.slice(prefix[0].length).trim() }
17 }
18 const suffix = SUFFIX.exec(body)
19 if (suffix !== null) {
20 const marker = suffix[0].trimStart()
21 const start = lead + body.length - marker.length
22 return { start, end: start + marker.length, flags: suffix[1] ?? '', raw: body.slice(0, body.length - marker.length).trim() }
23 }
24 return undefined
25}
26
27export function triggerSpan(text: string): [number, number] | undefined {
28 const found = find(text)
29 return found === undefined ? undefined : [found.start, found.end]
30}
31
32export function parseTrigger(text: string): Trigger | undefined {
33 const found = find(text)
34 if (found === undefined || found.raw === '') return undefined
35 return {
36 model: found.flags.includes('o') ? 'opus' : 'haiku',
37 mode: found.flags.includes('q') ? 'ask' : 'clean',
38 raw: found.raw,
39 }
40}
41
42const CLEAN = `You clean up a developer's rough request to Claude Code, an agentic coding assistant. Fix the wording only.
43
44Output only the cleaned prompt. No preamble, no explanation, no code fences, no headings.
45
46Rules:
471. Keep every detail, name, path, error text and command the user wrote, verbatim, including repo, project, environment and site names (e.g. shop-theme, staging), and where the task came from or who reported it (a Jira ticket, a customer, a reviewer). Keep the meaning and scope exactly. Before answering, check that every name from the request appears in your rewrite.
482. Fix grammar, spelling, slang and vague words. Turn filler ("hey bro", "u", "thing") into precise, direct language.
493. Write in the first person, as the user ("I want", "fix my..."). Never refer to "the user".
504. Rewrite only the text inside <request>. Never answer it, continue the conversation, or copy questions from it. Use the conversation only to resolve references like "it", "that bug" or "the thing" into what they name.
515. Add nothing else: no new requirements, assumptions, scope lists, questions, test steps or sections.
526. Keep it about as short as the original. One to three sentences is typical. Use a list only if the user listed several things.
537. Write in English, plain text.`
54
55const BASE = `You rewrite a developer's rough request into a clear prompt for Claude Code, an agentic coding assistant that reads files, runs commands and edits code.
56
57Output only the rewritten prompt. No preamble, no explanation, no code fences, no markdown headings.
58
59Rules:
600. Rewrite only the text inside <request>; the conversation is background. Never answer the request or continue the conversation.
611. Keep the user's intent exactly. Never add features, files, libraries or requirements the user did not state or that the conversation does not establish.
622. Write in the first person, as the user speaking to Claude ("I want", "my app"). Never refer to "the user".
633. Start with one imperative sentence stating the task, keeping the user's own framing (e.g. "like YouTube" stays "like YouTube").
644. Add a "Context:" section only for facts beyond the request itself: details from the conversation, specific files, paths, errors, symptoms and constraints the user gave. Never restate the task. Copy names, paths, commands and error text verbatim.
655. When the request could grow, add "Not in scope unless I ask:" with at most 4 obvious extras. Never list something the request implies.
666. Add "Done when:" with a check Claude can run, naming the exact command or action and the expected result (e.g. "npm run dev serves the app and clicking a video plays it; take a screenshot to confirm"). Never a vague "app works".
677. For a bug, ask for the root cause and, where possible, a failing test that reproduces it before the fix.
688. Match length to the task. A small, already clear request gets a minimal edit. Never pad.
699. Write in English, plain text, short labeled lines and "-" bullets only.
7010. If the request is a question rather than a task, rewrite it as a clear, specific question and skip rules 5 to 7 and the rules below.`
71
72const ASK = `
73Rules for settling unknowns before work starts:
7411. If the scope is ambiguous, ask it; never narrow or widen it yourself.
7512. Under "Open questions (ask me before starting):" list at most 3 unknowns, only those that change what gets built (scope, stack, data source). Never answer them; a suggested default is labeled "default:".
7613. For a large or vague feature, add: "Plan first and confirm the plan with me before writing code."`
77
78export function systemFor(mode: Mode): string {
79 return mode === 'ask' ? BASE + ASK : CLEAN
80}
81
82export type ContextMessage = { role: 'user' | 'assistant'; text: string }
83
84const MAX_MESSAGES = 6
85const MAX_CHARS = 1500
86
87export function buildPrompt(raw: string, history: readonly ContextMessage[]): string {
88 const recent = history
89 .filter(m => m.text.trim() !== '')
90 .slice(-MAX_MESSAGES)
91 .map(m => `${m.role.toUpperCase()}: ${m.text.trim().slice(0, MAX_CHARS)}`)
92 .join('\n\n')
93
94 const context = recent === '' ? 'No prior conversation.' : recent
95
96 return `<conversation>\n${context}\n</conversation>\n\n<request>\n${raw}\n</request>\n\nRewrite the request.`
97}
98