SLOPSHOPPER

polish

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

newcommandtoaststatusmodel
v0.1.0no licenseupdated 2026-10-15HuliLulu46/cc-prompt-polish
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · polish
› fix the failing auth test and add an audit log call ╭────────────────────────────────────────────╮ │ polish │ ⏺ Read(src/auth.ts) │ <text> [-g guidance] rewrite | -r │ ⎿ Read 6 lines │ [guidance] rewrite again | -u restore │ ⏺ Update(src/auth.ts) │ original | -n ignore conversation | -c │ ⎿ Added 2 lines, removed 1 line ╰────────────────────────────────────────────╯ ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /polish ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

polish

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.

Usage

/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
  • The rewrite keeps the draft's meaning, language and technical details (paths, commands, identifiers) and adds no new requirements.
  • By default it reads the conversation, so references like "that file" become concrete. With no conversation yet, or with -n, it rewrites the text alone.
  • Only the last -g splits the guidance off, so a draft can contain things like npm i -g foo.
  • Flags combine, such as -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.
  • Notices appear as toasts; if the input box is busy, the result shows in a toast instead.

Install

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.

Known limits

  • -r and -u remember only the last rewrite of the current session, and forget it when the session restarts.
  • The rewrite without conversation uses sonnet.

Built against Claude Code 2.1.300. A mod is an early-access API that may change between releases.

Develop

claude plugin validate .
claude plugin test .
tsc -p .
Source 1 files
hooks/register.ts 222 lines
1import 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