SLOPSHOPPER

prompt-boost

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

newbandcommandtoastpromptmodel
v0.4.1MITupdated 2026-10-06GururOzatmaca/claude-mods/prompt-boost
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · prompt-boost
› 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 › /ap ⎿ prompt-boost: No rewrite yet. Prefixes: ++ (clean wording, Haiku), ++o (clean, Opus), ++q (structured + questions), ++oq (Op ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-mods

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.

prompt-boost

Add ++ to a messy prompt. It gets rewritten in clear English and put back in the box. You check it, then press Enter.

Typing a prompt with ++ Rewritten prompt in the box

  • ++ at the start or end. ++o uses Opus, ++q adds open questions.
  • Only the wording changes. Names, paths and errors stay exactly as you wrote them.
  • /ap undo brings back your original.

reply-tools

A copy button on every code block, and a tl;dr button under long replies.

Reply with copy and tl;dr buttons tl;dr of the reply

  • ⧉ copy all copies several shell commands at once.
  • tl;dr keeps every step and warning. ↑ original switches back.
  • Turn parts off with /plugin configure reply-tools@claude-mods.

jump-list

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.
  • Works after resume too. Open it with /jumps.
  • A very old prompt may need a second click.

Good to know

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.

Source 3 files
hooks/register.tsx 137 lines
1import 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}
137
hooks/rainbow.ts 2 lines
1export const ORANGE = '#ff8c00'
2
hooks/rules.ts 98 lines
1export 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