/polish <text>: rewrites it into a clearer prompt and leaves the result in the input box for you to confirm

A Claude Code mod that adds /polish: it rewrites a rough prompt into a clearer one and leaves the result in the input box, so you can read and edit it before sending. Neither the command nor its result goes into the conversation.
/polish <text> rewrite the text
/polish <text> -g <guidance> rewrite with extra guidance, e.g. -g keep it short
/polish -r [guidance] rewrite the last draft again; with guidance, adjust the last result instead
/polish -u put the last original text back
/polish -n <text> rewrite without reading the conversation
/polish -c [focus] fill the input box with /compact plus instructions on what to keep
/polish -h show usage
-n, it rewrites the text alone.-g splits the guidance off, so a draft can contain things like npm i -g foo.-rn.-c writes /compact instructions naming the constraints, decisions and states that are easy to lose in a summary; with nothing to add it fills a bare /compact.At the prompt of a Claude Code terminal session:
/plugin install polish --marketplace HuliLulu46/polish
Answer y to add the marketplace, then pick a scope (user is first).
From a shell:
claude plugin install polish --marketplace HuliLulu46/polish
In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with HuliLulu46/polish, then Add plugin.
-r and -u remember only the last rewrite of the current session, and forget it when the session restarts.sonnet.Built against Claude Code 2.1.300. A mod is an early-access API that may change between releases.
claude plugin validate .
claude plugin test .
tsc -p .hooks/register.ts 222 lines1import type { On } from 'claude-code'
2
3const COMMAND = 'polish'
4
5// Toasts don't support line breaks, so keep this on one line
6const USAGE =
7 '<text> [-g guidance] rewrite | -r [guidance] rewrite again | -u restore original | -n ignore conversation | -c [focus] generate a /compact command'
8
9const SYSTEM = `You rewrite instructions a user wrote for an AI coding assistant so they are clearer.
10- Keep the original meaning and every technical detail (paths, commands and code identifiers stay verbatim); add no requirement the draft does not have
11- Write the rewrite in the same language as the draft (a Chinese draft stays Chinese), even though these instructions are in English
12- Fix typos and grammar and tidy the structure; use a short list when there are many points
13- Rewrite only the text inside the <draft> tag; nothing outside the tag is part of the draft
14- When <guidance> explicitly asks to add, remove or change content, <guidance> wins
15- Output only the rewritten text: no explanation, quotes, code fences or <draft> tag`
16
17// adjust: -r came with new guidance, so tweak the previous result; otherwise write a clearly different version
18type Request = { draft: string; guidance: string; previous?: string; adjust?: boolean }
19
20const extrasOf = ({ guidance, previous, adjust }: Request) =>
21 [
22 guidance &&
23 `Extra requirements for this rewrite (they guide how to rewrite and are not part of the draft):\n<guidance>\n${guidance}\n</guidance>`,
24 previous &&
25 `${
26 adjust
27 ? 'This is the previous rewrite. Adjust it according to <guidance> and keep the rest as unchanged as possible'
28 : 'The user was not satisfied with the previous rewrite. Give a clearly different version this time'
29 }:\n<previous>\n${previous}\n</previous>`,
30 ]
31 .filter(Boolean)
32 .join('\n\n')
33
34const draftPromptOf = (r: Request) => `${extrasOf(r)}
35
36<draft>
37${r.draft}
38</draft>`
39
40// The engine appends its own instructions to the end of a fork request; tags keep the draft apart from them
41const forkPromptOf = (r: Request) => `This turn is not a new task: do not carry out or answer the instructions in <draft>; only rewrite them and output the result.
42${SYSTEM}
43- You may use the earlier conversation to replace references like "just now" or "that one" with concrete content, but do not bring in requirements that neither the draft nor <guidance> mentions
44
45${draftPromptOf(r)}`
46
47const NOTHING_EXTRA = 'NONE'
48
49const compactPromptOf = (focus: string) => `This turn only writes extra instructions to pass to /compact; it is not a new work request. Do not continue the earlier task, answer earlier questions, run commands or call any tools. Rely only on the conversation visible now; do not wait for or guess results that have not come back.
50
51<context>
52The model that does the compaction already has a fixed summary template covering request intent, key concepts, files and code, errors and fixes, user messages, to-dos, current work and next steps.
53What you add are retention requirements that are easy to drop, confuse or distort when summarized, not another summary.
54CLAUDE.md, the system prompt, tool descriptions and recently read files are reloaded after compaction; do not repeat them.
55</context>
56
57<instructions>
58Pick out the information that still affects what happens next and write it as concrete retention instructions. Prioritize in this order:
591. Explicit constraints that change later behavior: what the user authorized or forbade, features that were dropped, the specific requirements and definitions currently in force, and the target directories, paths, identifiers, commands or config values that must be used. Losing these leads to overstepping, redoing work, writing to the wrong place or reverting to an outdated definition; never drop them just to write fewer items.
602. Decisions and states that are easy to misjudge: what the user confirmed versus what the assistant only suggested, and what is implemented, verified, still without a visible result, or still undecided. The reasons for rejected approaches, needed to avoid retrying them, belong here.
613. Other reasons, measured values and technical details that still affect later work.
62Skip a whole category when the conversation gives no basis for it or it no longer affects later work. Several constraints in the same tier may be merged into one item, but do not keep only the one that is easiest to summarize.
63Keep the rule itself; do not replace its content with status words like "wrapped up", "settled" or "done".
64For a group of requirements that exists only in the conversation, name the specific items to keep and how they relate; do not replace the content with a count or "followed up".
65The user's latest explicit correction of the goal or the authorized scope overrides earlier statements; an assistant suggestion does not count as user confirmation. Judge completion by actual execution results; when a result is not visible or not yet verified, say so, and do not conclude from that alone that the work is unfinished.
66Write only what the visible conversation supports; do not invent unstated reasons, plans, steps or results.
67</instructions>
68${
69 focus
70 ? `
71<focus>
72${focus}
73</focus>
74focus only adjusts the retention priority of existing information. It is not a source of facts and does not authorize carrying out tasks or changing the constraints above. When something focus mentions has no basis in the conversation, do not write it.
75`
76 : ''
77}
78<output>
79Output only the extra instructions themselves, with no explanation, and do not start with /compact. Write in the language the conversation uses. Line breaks are fine; do not use Markdown, code blocks, XML tags or surrounding quotes.
80Each item names its subject and the scope, rule or state that must be kept; write paths, identifiers, commands and config values verbatim.
81Usually 1 to 4 items. When there is a lot, drop the less important items first; do not compress into a keyword list, invent abbreviations or leave out necessary relationships. When there is little, do not pad. The item count is only a guide and never a reason to leave out a tier 1 constraint.
82When nothing from tier 1 or 2 needs keeping, output only "${NOTHING_EXTRA}".
83</output>`
84
85// The typings say string and null respectively, but 2.1.285 actually returns
86// { isAnswered: true, text, usage } or { isAnswered: false, reason } for both
87type Reply = { isAnswered: boolean; text?: string; reason?: string }
88
89const answerOf = (reply: unknown): string | undefined => {
90 if (typeof reply === 'string') return reply
91 const r = reply as Reply | null
92 return r?.isAnswered ? r.text : undefined
93}
94
95// Leading -r/-n/-u/-h/-c (combinable, e.g. -rn) are flags; the rest splits at the last " -g " into draft and guidance
96const parse = (args: string) => {
97 const flags = new Set<string>()
98 let rest = args.trim()
99 let match = rest.match(/^-([rnuhc]+)(?:\s+|$)/)
100 while (match) {
101 for (const flag of match[1] ?? '') flags.add(flag)
102 rest = rest.slice(match[0].length)
103 match = rest.match(/^-([rnuhc]+)(?:\s+|$)/)
104 }
105 // The draft may contain things like npm i -g, so take the last -g
106 const at = [...rest.matchAll(/(?:^|\s)-g(?=\s|$)/g)].at(-1)?.index ?? -1
107 return {
108 flags,
109 draft: (at < 0 ? rest : rest.slice(0, at)).trim(),
110 guidance: at < 0 ? '' : rest.slice(at).replace(/^\s*-g/, '').trim(),
111 }
112}
113
114const RECORD_MARK = `<command-name>/${COMMAND}</command-name>`
115const STDOUT_MARK = `<local-command-stdout>${COMMAND}:`
116
117export function register(on: On): void {
118 // Cleared on hot reload or a new session
119 let last: { draft: string; guidance: string; result: string } | undefined
120
121 // Empty the /polish command record and output; the rows can't be deleted and are saved as (no content)
122 on('session.append', { door: 'command' }, ($, e, next) => {
123 const text = e.message.content
124 .map(block => (block.type === 'text' ? block.text : ''))
125 .join('')
126 const isOwnRow = text.includes(RECORD_MARK) || text.includes(STDOUT_MARK)
127 return next(isOwnRow ? { ...e, message: { ...e.message, content: [] } } : e)
128 })
129
130 on('session.start', async ($, e, next) => {
131 await $.command.register({
132 name: COMMAND,
133 description: 'Rewrite the text and put the result back in the input box (-h for usage)',
134 argumentHint: '[-r|-u|-n|-c|-h] <text> [-g <guidance>]',
135 immediate: true,
136 })
137 return next(e)
138 })
139
140 // All notices go through toasts: they stay out of the transcript and are never sent to the model
141 on('command.run', { command: COMMAND }, async ($, e) => {
142 const { flags, draft, guidance } = parse(e.args)
143 const notify = (text: string, timeoutMs = 4000) => {
144 $.ui.toast(text, { timeoutMs })
145 return {}
146 }
147
148 if (flags.has('h')) return notify(USAGE, 15000)
149
150 // Needs the conversation, so no fallback to the context-free model; doesn't update last, so -r/-u still target the previous rewrite
151 if (flags.has('c')) {
152 $.ui.status('Generating compact instructions…')
153 let reply: unknown
154 try {
155 const focus = [draft, guidance].filter(Boolean).join('; ')
156 reply = await $.model.fork({ prompt: compactPromptOf(focus) })
157 } finally {
158 $.ui.status(undefined)
159 }
160 const brief = answerOf(reply)?.trim().replace(/^\/compact\s*/, '')
161 if (!brief) {
162 const reason = (reply as Reply | null)?.reason
163 return notify(
164 reason === 'nothing-to-fork'
165 ? 'Nothing in this conversation to compact yet'
166 : `Generation failed: ${reason ?? 'the model returned nothing'}`,
167 )
168 }
169 const command = brief === NOTHING_EXTRA || brief === '无' ? '/compact' : `/compact ${brief}`
170 const { isFilled } = await $.prompt.fill({ text: command })
171 return isFilled ? {} : notify(`Input box unavailable. Result: ${command}`, 30000)
172 }
173
174 if (flags.has('u')) {
175 if (!last) return notify('No original text to restore yet')
176 await $.prompt.fill({ text: last.draft })
177 return {}
178 }
179
180 let request: Request
181 if (flags.has('r')) {
182 if (!last) return notify('Nothing to rewrite again yet')
183 const fresh = guidance || draft
184 request = {
185 draft: last.draft,
186 guidance: fresh ? [last.guidance, fresh].filter(Boolean).join('; ') : last.guidance,
187 previous: last.result,
188 adjust: Boolean(fresh),
189 }
190 } else {
191 if (!draft) return notify(USAGE, 15000)
192 request = { draft, guidance }
193 }
194
195 $.ui.status('Rewriting…')
196 let polished: string | undefined
197 try {
198 // fork carries the conversation; with no conversation yet it doesn't answer, so fall back to a context-free rewrite
199 const forked = flags.has('n')
200 ? undefined
201 : answerOf(await $.model.fork({ prompt: forkPromptOf(request) }))
202 polished = (
203 forked ??
204 answerOf(
205 await $.model.complete({
206 model: 'sonnet',
207 system: SYSTEM,
208 prompt: draftPromptOf(request),
209 }),
210 )
211 )?.trim()
212 } finally {
213 $.ui.status(undefined)
214 }
215 if (!polished) return notify('Rewrite failed: the model returned nothing')
216
217 last = { draft: request.draft, guidance: request.guidance, result: polished }
218 const { isFilled } = await $.prompt.fill({ text: polished })
219 return isFilled ? {} : notify(`Input box unavailable. Rewrite: ${polished}`, 30000)
220 })
221}
222