SLOPSHOPPER

prompt-enhancer

Rewrites the draft in the prompt box into a clearer prompt using the active project's context, without sending it; one press restores the original

newbandcommandtoastpromptmodel
v?MITupdated 2026-10-16Abdoul-sudo/prompt-enhancer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · prompt-enhancer
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ 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 › /enhance ⎿ prompt-enhancer: Type a prompt first, then enhance it. ▣ client module ./color_button.tsx ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
▣ client module ./color_button.tsx ⟨Claude Code's own drawing⟩
README

Prompt Enhancer

A Claude Code mod that turns a vague prompt into a clear one, using the project you are working in.

Type a prompt, click the orange Enhance prompt button above the prompt box, review the result, then send it yourself. Restore original puts your text back.

From the keyboard, type /enhance <prompt> to do the same.

A vague draft, then the same draft after one click on Enhance prompt

Install

In a Claude Code terminal session, run:

/plugin install prompt-enhancer --marketplace Abdoul-sudo/prompt-enhancer

Answer y to add the marketplace, then pick the user scope. The mod then loads in every session, the desktop app's Code tab included.

Better results: project-context.md

The mod gives better rewrites when your project has a project-context.md file: a short description of the project, its main parts, its key files and the names you use. The file goes in the folder where you start Claude Code. The mod reads up to 20,000 characters of it.

  • Create it: when the file is missing, a blue Create project context button shows to the left of Enhance prompt. Click it once: Claude writes the file with the mod's project-context skill.
  • Keep it current: Claude uses the same skill on its own to update the file after changes that add, remove or rename features, parts or key files. It never creates the file on its own. You can also run /prompt-enhancer:project-context at any time.

The blue Create project context button above the prompt box

How it works

  • The enhancer reads the draft in your prompt box.
  • The AI reads the last 8 exchanges of your conversation: your messages and Claude's replies.
  • The AI reads your project-context.md if it exists, up to 20,000 characters.
  • The AI rewrites the prompt for the model you use in the current session.
  • The enhancer puts the new prompt in your prompt box. Nothing is sent.

Model

SettingValueWhy
Modelsonnet (the newest Claude Sonnet)Good judgment about what to keep and what to add, at a fraction of Opus's cost and wait.
EffortmediumAs fast as low on short drafts in testing (a few seconds), with shorter and cleaner rewrites.

Each click uses your Claude Code account, like any other request.

Change the model or effort

  1. Open hooks/register.tsx and edit the two lines at the top:
   const MODEL = 'sonnet'
   const EFFORT = 'medium'
  • MODEL: an alias (haiku, sonnet, opus) or a full model id such as claude-opus-5-5.
  • EFFORT: low, medium, high, xhigh or max. Higher is slower and costs more.
  1. Start a new session, or run /reload-plugins in a terminal session.

An update replaces the installed files, so edits made there are lost when you update. To keep your own settings, fork the repository and install from your fork.

Update

Claude Code does not update this mod on its own by default: background auto-update is off for every marketplace you add yourself, and a marketplace cannot turn it on for you.

Turn on automatic updates (recommended)

Do this once:

  1. In a Claude Code terminal session, run /plugin.
  2. Open Marketplaces and select prompt-enhancer.
  3. Select Enable auto-update.

Claude Code then checks for a new version in the background after a session starts. A new version applies from the next session.

Update by hand

In your shell:

claude plugin marketplace update prompt-enhancer
claude plugin update prompt-enhancer@prompt-enhancer

Then start a new session.

Files

PathWhat it holds
.claude-plugin/plugin.jsonThe mod's manifest.
.claude-plugin/marketplace.jsonMakes this repository its own marketplace, so it installs with one line.
hooks/register.tsxThe mod: the button, the rewrite instructions and the model call.
hooks/color_button.tsxThe colored buttons, drawn as their own region so the desktop app keeps their color.
skills/project-context/SKILL.mdThe skill that creates and updates project-context.md.
types/index.d.tsThe types of the values the mod keeps for the session.
tests/enhance.test.tsxTests, run with claude plugin test .
docs/before-after.pngThe before and after example image.
docs/create-project-context.pngThe Create project context button image.

License

MIT

Source 3 files
hooks/register.tsx 292 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4const hasDraft = atom({ plugin: 'prompt-enhancer', key: 'hasDraft' } as const, null)
5const isBusy = atom({ plugin: 'prompt-enhancer', key: 'isBusy' } as const, false)
6const original = atom({ plugin: 'prompt-enhancer', key: 'original' } as const, null)
7const hasProjectContext = atom({ plugin: 'prompt-enhancer', key: 'hasProjectContext' } as const, null)
8
9const MAX_MESSAGE_CHARS = 2000
10/** One exchange is the user's message and the agent's reply. */
11const RECENT_EXCHANGES = 8
12/** The project description the mod reads; its `project-context` skill writes it. */
13const PROJECT_CONTEXT_FILE = 'project-context.md'
14const MAX_CONTEXT_FILE_CHARS = 20000
15const CREATE_CONTEXT_PROMPT = 'Create project-context.md for this project with the prompt-enhancer project-context skill.'
16/** Alias the engine resolves to the newest Sonnet, as `--model sonnet` does. */
17const MODEL = 'sonnet'
18/** Tested: as fast as low on short drafts, with shorter and cleaner rewrites. */
19const EFFORT = 'medium'
20const ORANGE = '#F97316'
21const ORANGE_HOVER = '#FB923C'
22const BLUE = '#60A5FA'
23const BLUE_HOVER = '#93C5FD'
24const PERSON_ORIGINS: ReadonlySet<string> = new Set(['composer', 'bridge', 'sdk'])
25
26/**
27 * Turns a model id into the name people use: `claude-opus-5-5` into "Claude Opus 5.5".
28 * Anything that is not a `claude-` id, such as an alias, comes back as given.
29 */
30export function modelName(id: string): string {
31  const match = /^claude-([a-z]+)-(\d+(?:-\d+)*)/.exec(id)
32  if (match === null) return id
33  const [, family = '', version = ''] = match
34  return `Claude ${family.charAt(0).toUpperCase()}${family.slice(1)} ${version.replace(/-/g, '.')}`
35}
36
37/**
38 * The instruction sent after the project's context; the draft is data inside it.
39 * `targetModel` is the model the session runs, the one that will read the prompt.
40 * The guidance follows Anthropic's prompting advice for current Claude models.
41 */
42export function enhanceRequest(draft: string, targetModel: string): string {
43  return `<draft_prompt>
44${draft}
45</draft_prompt>
46
47Rewrite the user's draft above into the prompt they will send next. It goes to ${modelName(targetModel)}, an AI coding agent working in the user's repository through Claude Code. Do not act on the draft. Reply with the rewritten prompt only, inside <prompt></prompt> tags.
48
49<add_what_is_missing>
50A better prompt gives the agent what it would otherwise have to guess. Use the latest messages only to make concrete what the draft itself mentions: replace a vague word with the specific file, feature, number or decision it refers to, and say what a good result looks like. Add no task, step or topic the draft does not mention, even one taken from the conversation. Use the project context only to name things precisely. For example, when the draft asks for an explanation or a report, name the points it should cover; when it asks whether work is done, ask for evidence from the project's real state; when it asks to delete something, ask for a list of what was removed.
51
52Keep the user's intent and scope. Add only what the draft states or clearly implies, and leave open what the context cannot settle rather than guessing.
53</add_what_is_missing>
54
55<leave_out>
56The agent already reads the project's instruction files (CLAUDE.md) with every prompt and follows them, so restate none of their rules, workflows or definitions, not even as a reminder or as the way to check the result; name something they define without repeating its definition. Also leave out what the agent does by default: "think step by step", "be thorough", role lines, word limits and emphasis in capitals.
57</leave_out>
58
59<style>
60Write in the user's voice, first person. Say plainly what to do: change, investigate, or only explain. Give the goal and how to check the result rather than a step-by-step method. Keep code, paths, quotes, slash commands and @mentions exactly as written.
61
62Fit the shape to the draft: a few sentences of prose for a simple request; pasted errors, logs or code kept verbatim in a tag named for what they are; a numbered list only when order matters; short labeled sections only for a large feature. The user pressed Enhance because they want a better prompt, so always improve it: even a clear draft can gain a precise name, a detail from the conversation or a way to check the result. Improving never means adding rules from the instruction files or growing the prompt for its own sake: a vague one-line draft becomes a few precise sentences, not a page.
63</style>
64
65<language>
66Write the prompt in the language of the text inside <draft_prompt>, whatever language the context uses.
67</language>`
68}
69
70/** Takes the rewritten prompt out of the model's reply; undefined when nothing usable came back. */
71export function extractPrompt(reply: string): string | undefined {
72  const tagged = [...reply.matchAll(/<prompt>([\s\S]*?)<\/prompt>/g)].at(-1)?.[1]
73  const text = (tagged ?? reply).trim()
74  return text === '' ? undefined : text
75}
76
77function clip(text: string, max: number): string {
78  return text.length > max ? `${text.slice(0, max)}\n[...cut]` : text
79}
80
81/**
82 * The latest exchanges of the conversation, the user's messages and the agent's
83 * replies alike. User rows that are machine text (task notices, reminders) start
84 * with a tag and are left out.
85 */
86export function recentMessages<M extends { role: string; text: string }>(messages: readonly M[]): M[] {
87  return messages
88    .filter(m => m.text.trim() !== '' && !(m.role === 'user' && m.text.trimStart().startsWith('<')))
89    .slice(-2 * RECENT_EXCHANGES)
90}
91
92/**
93 * The project's context, read live: project-context.md and the end of the conversation.
94 * CLAUDE.md, skills and tools are left out: the agent that runs the prompt already has them.
95 */
96async function projectContext($: EngineInterface): Promise<string> {
97  const parts: string[] = [`Working directory: ${await $.session.cwd()}`]
98  // project-context.md describes the project itself (what it is, its parts, its files), so it is sent whole.
99  const contextPath = await projectContextPath($)
100  const contextText = await $.fs.read(contextPath).catch(() => '')
101  if (contextText.trim() !== '') {
102    parts.push(`<project_context path="${contextPath}">\n${clip(contextText, MAX_CONTEXT_FILE_CHARS)}\n</project_context>`)
103  }
104  const messages = await $.session.messages().catch(() => [])
105  const recent = recentMessages(Array.isArray(messages) ? messages : [])
106  if (recent.length > 0) {
107    const lines = recent.map(m => `<${m.role}>\n${clip(m.text, MAX_MESSAGE_CHARS)}\n</${m.role}>`)
108    parts.push(`The conversation so far (latest messages):\n${lines.join('\n')}`)
109  }
110  return `You are a prompt enhancer. You turn a developer's draft into the instruction they send to their AI coding agent in this project. Your whole reply is that instruction, written as the developer would send it: no explanation, commentary or answer of your own. The project's context:\n\n${parts.join('\n\n')}`
111}
112
113/** Rewrites the draft with one Sonnet call at medium effort, fed the project's context. */
114async function rewrite($: EngineInterface, draft: string): Promise<{ text: string } | { reason: string }> {
115  const reply = await $.model.complete({
116    model: MODEL,
117    effort: EFFORT,
118    system: await projectContext($),
119    prompt: enhanceRequest(draft, await $.session.model()),
120    maxTokens: 4000,
121    timeoutMs: 120000,
122  })
123  if (!reply.isAnswered) return { reason: reply.reason }
124  const text = extractPrompt(reply.text)
125  return text === undefined ? { reason: 'empty-reply' } : { text }
126}
127
128/**
129 * Enhances `draft`, or the prompt box's draft when none is given, and puts the
130 * result in the box without sending it. Answers a line for the caller to show.
131 */
132async function enhance($: EngineInterface, draft?: string): Promise<string> {
133  if (await read($, isBusy)) return 'Already enhancing.'
134  const source = draft ?? (await $.prompt.read()).text
135  if (source.trim() === '') return 'Type a prompt first, then enhance it.'
136  await update($, isBusy, () => true)
137  try {
138    const result = await rewrite($, source)
139    if ('reason' in result) return `Could not enhance the prompt (${result.reason}).`
140    const box = await $.prompt.read()
141    const isOverwritingEdits = draft === undefined ? box.text !== source : box.text.trim() !== ''
142    if (isOverwritingEdits) return 'The prompt box changed while enhancing. Your text was kept.'
143    const filled = await $.prompt.fill({ text: result.text })
144    if (!filled.isFilled) {
145      const copied = await $.ui.copy({ text: result.text })
146      return copied.isCopied ? 'The prompt box is busy. The enhanced prompt is on the clipboard.' : 'The prompt box is busy. Try again.'
147    }
148    await update($, original, () => source)
149    await update($, hasDraft, () => true)
150    return 'Prompt enhanced. Review it, then send.'
151  } finally {
152    await update($, isBusy, () => false)
153  }
154}
155
156async function restore($: EngineInterface): Promise<void> {
157  const text = await read($, original)
158  if (text === null) return
159  const filled = await $.prompt.fill({ text })
160  if (!filled.isFilled) {
161    $.ui.toast('The prompt box is busy. Try again.')
162    return
163  }
164  await update($, original, () => null)
165}
166
167/**
168 * Where the project's project-context.md lives: the folder the session started in,
169 * never a parent folder, so a file from another project is never read.
170 */
171async function projectContextPath($: EngineInterface): Promise<string> {
172  return `${await $.session.root()}/${PROJECT_CONTEXT_FILE}`
173}
174
175/** Whether the project has a project-context.md, so the band offers to create one only when it is missing. */
176async function refreshProjectContext($: EngineInterface): Promise<void> {
177  const exists = await $.fs.exists(await projectContextPath($)).catch(() => false)
178  await update($, hasProjectContext, () => exists)
179}
180
181/** Asks Claude to write project-context.md with the mod's skill, as a turn the user watches. */
182async function createProjectContext($: EngineInterface): Promise<void> {
183  await $.prompt.submit({ text: CREATE_CONTEXT_PROMPT })
184}
185
186export const register: Register = on => {
187  let knownDraft: boolean | null = null
188
189  on('session.start', async ($, e, next) => {
190    await update($, isBusy, () => false)
191    await refreshProjectContext($)
192    await $.command.register({
193      name: 'enhance',
194      description: 'Rewrite a prompt into a clearer one for this project and put it in the prompt box',
195      argumentHint: '<prompt>',
196    })
197    return next(e)
198  })
199
200  // Tracks only whether the box is empty, so the band shows when there is something to enhance.
201  on('prompt.edit', async ($, e, next) => {
202    const box = await next(e)
203    const hasText = box.text.trim() !== ''
204    if (hasText !== knownDraft) {
205      knownDraft = hasText
206      await update($, hasDraft, () => hasText)
207    }
208    return box
209  }).catch(($, e, next) => next(e))
210
211  // Only a prompt the person sends empties their box; task notices, schedules and peers leave it as it is.
212  on('prompt.submit', async ($, e, next) => {
213    if (!PERSON_ORIGINS.has(e.origin.kind)) return next(e)
214    knownDraft = false
215    await update($, hasDraft, () => false)
216    await update($, original, () => null)
217    return next(e)
218  })
219
220  on('command.run', { command: 'enhance' }, async ($, e) => ({ text: await enhance($, e.args) }))
221
222  // The orange Client button posts 'press' when clicked or when Enter reaches it.
223  // A turn may have created or removed project-context.md.
224  on('turn.complete', async ($, e, next) => {
225    if (e.agentId === undefined) await refreshProjectContext($).catch(() => undefined)
226    return next(e)
227  })
228
229  on('ui.message', { element: 'create-context' }, async ($, e, next) => {
230    if (e.data === 'press') await createProjectContext($)
231    return next(e)
232  })
233
234  on('ui.message', { element: 'enhance' }, async ($, e, next) => {
235    if (e.data === 'press') $.ui.toast(await enhance($))
236    return next(e)
237  })
238
239  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
240    if (e.props.hasSurvey) return next(e)
241    const busy = await read($, isBusy)
242    const draft = await read($, hasDraft)
243    const before = await read($, original)
244    const canEnhance = !busy && draft !== false
245    const canCreateContext = !busy && (await read($, hasProjectContext)) === false
246    if (!busy && !canEnhance && before === null && !canCreateContext) return next(e)
247    const elements = $.ui.resolve(e)
248    const { Box, Button, Text } = elements
249    const Client = 'Client' in elements ? elements.Client : undefined
250    const below = await next(e)
251    return (
252      <Box flexDirection="column">
253        <Box flexDirection="row" justifyContent="flex-end" width="100%" gap={1}>
254          {canCreateContext &&
255            (Client !== undefined ? (
256              <Client
257                key="create-context"
258                module="./color_button.tsx"
259                props={{ label: 'Create project context', color: BLUE, hoverColor: BLUE_HOVER }}
260              />
261            ) : (
262              <Button key="create-context" onPress={() => createProjectContext($)}>
263                <Text color={BLUE} bold>
264                  Create project context
265                </Text>
266              </Button>
267            ))}
268          {busy && <Text color={ORANGE}>Enhancing prompt…</Text>}
269          {canEnhance &&
270            (Client !== undefined ? (
271              <Client
272                key="enhance"
273                module="./color_button.tsx"
274                props={{ label: 'Enhance prompt', color: ORANGE, hoverColor: ORANGE_HOVER }}
275              />
276            ) : (
277              <Button key="enhance" hotkey="e" onPress={async () => $.ui.toast(await enhance($))}>
278                <Text color={ORANGE} bold>
279                  Enhance prompt
280                </Text>
281              </Button>
282            ))}
283          {!busy && before !== null && (
284            <Button key="restore" label="↶ Restore original" hotkey="r" dimColor onPress={() => restore($)} />
285          )}
286        </Box>
287        {below}
288      </Box>
289    )
290  })
291}
292
hooks/color_button.tsx 32 lines
1import type { ClientModule } from 'claude-code'
2
3type Props = { label: string; color: string; hoverColor: string }
4type State = { isHovered: boolean }
5
6/**
7 * A clickable label on a colored background. The desktop draws every `Button`
8 * as its own native control and ignores colors inside it; a `Client` region is
9 * drawn with the plugin's own colors, so the press is posted to the hooks module.
10 */
11const ColorButton: ClientModule<Props, State> = (props, surface) => {
12  const { Box, Text } = surface.elements
13  const isHovered = surface.state?.isHovered ?? false
14  const isInside = (x: number, y: number) => x >= 0 && y >= 0 && x < surface.columns && y < surface.rows
15  surface.onPointer(e => {
16    if (e.type === 'enter' || e.type === 'leave') surface.setState({ isHovered: e.type === 'enter' })
17    if (e.type === 'up' && e.button === 'left' && isInside(e.x, e.y)) surface.post('press')
18  })
19  surface.onKey(e => {
20    if (e.key === 'return' || e.key === ' ') surface.post('press')
21  })
22  return (
23    <Box backgroundColor={isHovered ? props.hoverColor : props.color} paddingX={2}>
24      <Text color="#000000" bold>
25        {props.label}
26      </Text>
27    </Box>
28  )
29}
30
31export default ColorButton
32
types/index.d.ts 17 lines
1/** The draft before the last rewrite, while it can be restored. */
2export type OriginalDraft = string | null
3
4declare module 'claude-code' {
5  interface PluginState {
6    'prompt-enhancer': {
7      /** Whether the prompt box holds text; null until the first edit is seen. */
8      hasDraft: boolean | null
9      /** True while a rewrite runs. */
10      isBusy: boolean
11      original: OriginalDraft
12      /** Whether the project has a project-context.md; null until checked. */
13      hasProjectContext: boolean | null
14    }
15  }
16}
17