SLOPSHOPPER

prompt-polish

Rewrites each prompt you send into a clear, well-structured prompt that follows Anthropic's prompting best practices, before Claude reads it

newbandcommandtoaststatusprompt
v0.4.0no licenseupdated 2026-10-26vorteclas/claude-prompt-polish
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · prompt-polish
› fix the failing auth test and add an audit log call ╭───────────────────╮ │ prompt-polish │ ⏺ Read(src/auth.ts) │ Prompt polish off │ ⎿ Read 6 lines ╰───────────────────╯ ⏺ Update(src/auth.ts) ⎿ 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 ⎿ prompt-polish: Prompt polish is off (model: sonnet). ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

prompt-polish

A Claude Code mod that rewrites every prompt you send into the prompt you meant to write, before Claude reads it. Like Wispr Flow, minus the voice.

It does more than fix grammar. It applies the prompting practices Anthropic recommends for current Claude models: it leads with what you want, keeps your reasons next to your requests, names the file or feature that "it" and "that" refer to, separates pasted logs and code from instructions, and uses a short list only when you asked for several things. It never adds requirements you didn't ask for, never answers your prompt, and keeps questions as questions.

  • Sees the conversation. The last several turns go along with your prompt, so references like "do the same for the other one" get resolved correctly.
  • Uses the prompt cache. The guide and the conversation turns are sent as a stable, append-only prefix with cache marks, so each prompt reuses what the previous one cached instead of paying for a fresh cache every time.
  • Shows its work. The rewritten prompt appears in a band above the prompt box, with how many tokens came from the cache, and a Hide button.
  • Keeps your words. Your original prompt is passed to Claude alongside the rewrite, and wins if the two disagree.
  • Start a prompt with :: to send it exactly as typed.
  • /polish off and /polish on turn it off and on (remembered across sessions).
  • Slash commands, prompts under 4 words, and messages from background tasks or other sessions are left alone.
  • If the polishing call fails or takes longer than 30 seconds, the prompt is sent as typed.

Choosing the model

When you install the mod you pick which model does the polishing:

OptionTrade-off
sonnet (default)Best balance of quality and speed
haikuFastest and cheapest
opusMost careful, slowest

Switch any time from the chat with /polish sonnet, /polish haiku or /polish opus. The choice is remembered across sessions and replaces the one made at install. Each prompt waits for one call to that model, billed like any other model call.

Install

At the prompt of a terminal session:

/plugin install prompt-polish --marketplace vorteclas/claude-prompt-polish

From a shell:

claude plugin install prompt-polish --marketplace vorteclas/claude-prompt-polish

In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with vorteclas/claude-prompt-polish, then Add plugin.

Source 3 files
hooks/register.tsx 194 lines
1import { atom, read, update } from 'claude-code'
2import type { ModelTextBlock, Register, SessionMessage } from 'claude-code'
3
4import type { Polished } from '../types'
5import { GUIDE, SYSTEM } from './guide'
6
7// Start a prompt with this to send it exactly as typed.
8const RAW_PREFIX = '::'
9const ENABLED_KEY = 'enabled'
10const MODEL_KEY = 'model'
11const MIN_WORDS = 4
12// Characters kept of each turn shown to the polishing model.
13const TURN_CHARS = 1500
14// The conversation window only grows, so each request reuses the cached turns
15// of the last one; past MAX_TURNS it restarts from the last KEEP_TURNS (one miss).
16const MAX_TURNS = 16
17const KEEP_TURNS = 8
18const NOTE = "The user's prompt was automatically rewritten"
19
20const last = atom({ plugin: 'prompt-polish', key: 'last' } as const, null)
21const isHidden = atom({ plugin: 'prompt-polish', key: 'isHidden' } as const, false)
22const windowStart = atom({ plugin: 'prompt-polish', key: 'windowStart' } as const, 0)
23
24type Turn = { role: 'user' | 'assistant'; text: string }
25
26// One entry per speaker change; a turn's text is the same on every read once
27// the turn is over, which keeps the cached prefix byte-identical.
28const toTurns = (rows: readonly SessionMessage[]): Turn[] => {
29  const turns: Turn[] = []
30  for (const row of rows) {
31    let text = row.text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
32    const note = text.indexOf(NOTE)
33    if (note >= 0) text = text.slice(0, note)
34    text = text.trim()
35    if (!text) continue
36    const prev = turns[turns.length - 1]
37    if (prev?.role === row.role) prev.text += `\n\n${text}`
38    else turns.push({ role: row.role, text })
39  }
40
41  // The person's words from the start, Claude's from the end, where it sums up.
42  return turns.map(t => ({
43    ...t,
44    text:
45      t.text.length <= TURN_CHARS
46        ? t.text
47        : t.role === 'user'
48          ? `${t.text.slice(0, TURN_CHARS)} […]`
49          : `[…] ${t.text.slice(-TURN_CHARS)}`,
50  }))
51}
52
53const MODELS = ['sonnet', 'haiku', 'opus']
54
55const label = (model: string) => model.charAt(0).toUpperCase() + model.slice(1)
56
57export const register: Register = (on, options) => {
58  const installed = typeof options.model === 'string' ? options.model : 'sonnet'
59  // A model picked with /polish <model> wins over the install option. Plugin
60  // options aren't /config rows, so $.config.set can't change that one.
61  const pickModel = (saved: unknown) =>
62    typeof saved === 'string' && MODELS.includes(saved) ? saved : installed
63
64  on('session.start', async ($, e, next) => {
65    await $.command.register({
66      name: 'polish',
67      description: 'Turn prompt polishing on or off, or pick the model that polishes',
68      argumentHint: 'on | off | sonnet | haiku | opus',
69    })
70
71    return next(e)
72  })
73
74  on('command.run', { command: 'polish' }, async ($, e) => {
75    const arg = e.args.trim().toLowerCase()
76
77    if (MODELS.includes(arg)) {
78      await $.store.set(MODEL_KEY, arg)
79      $.ui.toast(`Prompts are now polished by ${label(arg)}`)
80
81      return { text: `Prompts are now polished by ${label(arg)}.` }
82    }
83
84    const isEnabled =
85      arg === 'on' ? true : arg === 'off' ? false : (await $.store.get(ENABLED_KEY)) === false
86    await $.store.set(ENABLED_KEY, isEnabled)
87    if (!isEnabled) await update($, last, () => null)
88    $.ui.toast(`Prompt polish ${isEnabled ? 'on' : 'off'}`)
89    const model = pickModel(await $.store.get(MODEL_KEY))
90
91    return { text: `Prompt polish is ${isEnabled ? 'on' : 'off'} (model: ${model}).` }
92  })
93
94  on('prompt.submit', async ($, e, next) => {
95    const isUsers = ['composer', 'bridge', 'sdk'].includes(e.origin.kind)
96    if (e.command || e.isMeta || !isUsers) return next(e)
97
98    // A new prompt of the person's: the band shows this one's rewrite or nothing.
99    await update($, last, () => null)
100
101    if (e.text.startsWith(RAW_PREFIX)) {
102      return next({ ...e, text: e.text.slice(RAW_PREFIX.length).trimStart() })
103    }
104
105    const isEnabled = (await $.store.get(ENABLED_KEY)) !== false
106    if (!isEnabled || e.text.trim().split(/\s+/).length < MIN_WORDS) return next(e)
107
108    const model = pickModel(await $.store.get(MODEL_KEY))
109    $.ui.status(`Polishing prompt with ${label(model)}…`)
110    let text = ''
111    let cachedTokens = 0
112    let failure = 'empty reply'
113    try {
114      let turns: Turn[] = []
115      try {
116        turns = toTurns(await $.session.messages())
117      } catch {
118        // No transcript to read: polish the prompt on its own.
119      }
120      let start = await read($, windowStart)
121      if (start > turns.length || turns.length - start > MAX_TURNS) {
122        start = Math.max(0, turns.length - KEEP_TURNS)
123        await update($, windowStart, () => start)
124      }
125      const window = turns.slice(start)
126
127      // Guide, then turns, each prefix marked: the guide is always a hit, and
128      // the turns cached by the last request are a hit up to where it ended.
129      // (Marked blocks go in `system`: older hosts refuse them in `prompt`.)
130      const system: ModelTextBlock[] = [
131        { text: `${SYSTEM}\n\n${GUIDE}`, cache: true },
132        ...window.map((t, i): ModelTextBlock => {
133          const block = { text: `<turn role="${t.role}">${t.text}</turn>\n` }
134          return i === window.length - 1 ? { ...block, cache: true } : block
135        }),
136        { text: '</recent_conversation>' },
137      ]
138
139      const r = await $.model.complete({
140        model,
141        system,
142        prompt: `<prompt>\n${e.text}\n</prompt>`,
143        maxTokens: 4096,
144        effort: 'low',
145        timeoutMs: 30000,
146      })
147      cachedTokens = r.usage.cache_read_input_tokens
148      if (r.isAnswered) text = r.text.trim()
149      else failure = r.reason
150    } catch (err) {
151      failure = err instanceof Error ? err.message : String(err)
152    } finally {
153      $.ui.status(undefined)
154    }
155
156    if (!text) {
157      $.ui.toast(`Prompt sent as typed (polish failed: ${failure})`)
158      return next(e)
159    }
160    if (text === e.text.trim()) return next(e)
161
162    const polished: Polished = { original: e.text, text, model, cachedTokens }
163    await update($, last, () => polished)
164    await update($, isHidden, () => false)
165
166    // Keep the original beside it so Claude can recover anything the rewrite lost.
167    const original = `${NOTE} for clarity by the prompt-polish mod. Their original wording was:\n\n${e.text}\n\nIf the two disagree, the original wins.`
168    return next({ ...e, text, context: [...(e.context ?? []), original] })
169  })
170
171  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
172    const polished = await read($, last)
173    if (e.props.hasSurvey || polished === null || (await read($, isHidden))) return next(e)
174
175    const { Box, Button, Text } = $.ui.resolve(e)
176    const cache =
177      polished.cachedTokens > 0
178        ? ` · ${(polished.cachedTokens / 1000).toFixed(1)}k tokens from cache`
179        : ''
180
181    return (
182      <Box flexDirection="column">
183        <Box>
184          <Text dimColor>
185            ✨ {label(polished.model)} polished your prompt to:{cache}{' '}
186          </Text>
187          <Button key="hide" label="Hide" onPress={() => update($, isHidden, () => true)} />
188        </Box>
189        <Text>{polished.text}</Text>
190      </Box>
191    )
192  })
193}
194
hooks/guide.ts 108 lines
1// The fixed opening of every polishing request. It never changes between
2// calls, so it is the cached prefix: keep anything per-request out of it.
3export const SYSTEM =
4  'You are a prompt editor inside Claude Code. You turn what a developer typed into the prompt they meant to write, for another Claude model to act on.'
5
6export const GUIDE = `<guide>
7A developer typed a prompt for Claude, the coding assistant in Claude Code, often quickly, with typos, run-on sentences and half-formed thoughts. You rewrite it into the prompt they would have written with time to spare. Claude reads your version instead of theirs, so it must say everything they meant, in their voice, and nothing they didn't.
8
9Your rewrite follows the practices Anthropic recommends for prompting current Claude models:
10
11Be clear and direct. Open with what the developer wants done or answered. State it as a plain instruction or question, at normal volume: no capitals for emphasis, no "you must", no "IMPORTANT". Current models follow a calmly stated instruction exactly, and inflated emphasis makes them rigid.
12
13Give context and the reason behind the request. When the developer said why they want something, or the recent conversation makes it clear, keep that reason next to the request, because Claude makes better judgment calls when it knows the goal. Include the facts Claude needs to act: which file, feature, error, command or earlier decision the developer is talking about.
14
15Resolve references using the conversation. When the prompt says "it", "that", "the bug", "do the same for the other one" or "like before", and the recent conversation shows unambiguously what is meant, name it ("the login form in src/auth/LoginForm.tsx", "the prompt-polish mod"). When the conversation does not settle it, keep the developer's own words rather than guessing, since a wrong guess is worse than a vague reference Claude can ask about.
16
17Describe the goal and the constraints, not a method. Say what done looks like when the developer implied it (tests pass, the page renders on mobile, the answer fits in a paragraph). Leave the how to Claude unless the developer prescribed a method, a tool or an order of steps.
18
19Phrase instructions as what to do. "Keep the public API unchanged" works better than "don't break anything". Keep a prohibition when the developer actually stated one.
20
21Separate material from instructions. Put pasted code, logs, error output and long quotes in fenced code blocks or descriptive XML tags (<error_log>, <current_output>) so Claude can tell the request from the data. Reproduce that material exactly, byte for byte.
22
23Keep quotes as quotes. When the developer quotes something, often a line of Claude's own reply marked with "> ", the quoted words are not theirs: keep the "> " markers (or put the quote in a blockquote) and leave its wording untouched, so Claude can tell what it said from what the developer is saying about it. Never fold a quote into the developer's first-person text.
24
25Match structure to content. A single request stays one or two sentences. When the developer listed several distinct requirements, questions or steps, use a short list, numbered only where order matters. Use headings only for a genuinely long, multi-part prompt.
26
27Leave out scaffolding current models no longer need: "think step by step", role-play openers ("You are an expert..."), threats, flattery, or reminders to be thorough. Claude already plans and reasons on its own.
28
29What you must preserve:
30- The developer's intent, scope and every concrete detail: names, paths, numbers, versions, URLs, commands, code, error text.
31- The kind of message. A question stays a question; never turn it into a statement or an assumption, and never answer it. A request stays a request. A decision stays a decision.
32- Their voice: write in the first person, as the developer, in the language they wrote in. Keep their tone; a casual prompt can stay casual.
33- Their level of commitment. "Maybe use Redis" stays tentative; it does not become "Use Redis".
34
35What you must not do:
36- Add requirements, features, edge cases, tests or questions the developer did not ask for and the conversation does not imply. Rewriting is not scope creep.
37- Answer, solve or comment on the prompt. You only rewrite it.
38- Pad. The rewrite is usually about as long as the original, shorter when the original rambled. Length follows content.
39
40Short replies inside a conversation ("yes do it", "use the second option", "thanks, looks great", "what about the other file?") only get their spelling and grammar fixed, plus a resolved reference where the conversation makes it certain. They are answers to Claude, not new tasks.
41
42If the prompt is already clear and correct, return it unchanged.
43
44Output only the rewritten prompt: no preamble, no explanation, no quotation marks around it, no tags around it.
45</guide>
46
47<examples>
48These are illustrative, showing range rather than a template to copy.
49
50<example>
51<recent_conversation>
52<turn role="assistant">I added rate limiting to the /api/upload route using the existing Redis client.</turn>
53</recent_conversation>
54<prompt>ok now same thing for the download one but higher limit maybe 3x</prompt>
55<rewrite>Add the same rate limiting to the /api/download route, using the existing Redis client, with a limit about three times higher than the one on /api/upload.</rewrite>
56</example>
57
58<example>
59<prompt>why does the build keep failing on ci but not locally?? i think its the node version but not sure</prompt>
60<rewrite>Why does the build fail on CI but not locally? I suspect a Node version mismatch, but I'm not sure.</rewrite>
61</example>
62
63<example>
64<prompt>can u refactor the payment module its a mess, theres like 3 functions doing the same validation and also the stripe stuff is mixed in with paypal. dont change the public functions other teams use them. also tests have to still pass</prompt>
65<rewrite>Refactor the payment module:
66
671. Merge the three functions that duplicate the same validation into one.
682. Separate the Stripe code from the PayPal code.
69
70Keep the public functions unchanged, since other teams depend on them, and make sure the existing tests still pass.</rewrite>
71</example>
72
73<example>
74<prompt>this is the error i get
75TypeError: Cannot read properties of undefined (reading 'map')
76    at UserList (UserList.tsx:14:22)
77fix it</prompt>
78<rewrite>Fix this error:
79
80\`\`\`
81TypeError: Cannot read properties of undefined (reading 'map')
82    at UserList (UserList.tsx:14:22)
83\`\`\`</rewrite>
84</example>
85
86<example>
87<prompt>> I'll keep the old endpoint for now and remove it next week.
88
89no remove it now, nobody uses it</prompt>
90<rewrite>> I'll keep the old endpoint for now and remove it next week.
91
92No, remove it now. Nobody uses it.</rewrite>
93</example>
94
95<example>
96<recent_conversation>
97<turn role="assistant">Do you want the mod's repository to be public or private?</turn>
98</recent_conversation>
99<prompt>public pls</prompt>
100<rewrite>Public, please.</rewrite>
101</example>
102</examples>
103
104Next comes the recent conversation between the developer and Claude, for context only. The prompt to rewrite arrives as the user message, inside <prompt> tags.
105
106<recent_conversation>
107`
108
types/index.d.ts 17 lines
1export type Polished = {
2  original: string
3  text: string
4  model: string
5  cachedTokens: number
6}
7
8declare module 'claude-code' {
9  interface PluginState {
10    'prompt-polish': {
11      last: Polished | null
12      isHidden: boolean
13      windowStart: number
14    }
15  }
16}
17