SLOPSHOPPER

prompt-enhancer

Start a prompt with >> to be offered a clearer rewrite of it before it is sent

newtoaststatuspromptmodel
★ 4v0.1.0no licenseupdated 2026-10-13tatthien/dotfiles/claude/mods/prompt-enhancer
A shopper browsing a rack in a slop shop
README

.dotfiles

There is no magic here. The bash script just creates symbolic links from ~/dotfiles.

# >>> start

cd ~
git clone git@github.com:tatthien/dotfiles.git
cd dotfiles
./install.sh

# >>> done
Source 1 files
hooks/register.tsx 108 lines
1import type { EngineInterface, Register } from "claude-code";
2
3const MODEL = "sonnet";
4const MIN_WORDS = 4;
5const PREVIEW_CHARS = 72;
6const YES = "Yes, polish it";
7const NO = "No, send as is";
8// Only a prompt starting with this goes to the enhancer (the prefix is stripped); others are sent untouched.
9const ASK_PREFIX = ">>";
10
11const SYSTEM = `You rewrite prompts that a developer is about to send to Claude Code, a coding agent working in their repository.
12
13Make the prompt clearer and more actionable: state the goal, the scope, any constraints, and what a finished result looks like. Keep every fact, file path, name and requirement the developer gave, keep their intent and voice, and do not invent details they did not give. If the prompt is already clear, return it almost unchanged.
14
15Reply with the rewritten prompt only: no preamble, no quotes, no explanation.`;
16
17export const register: Register = (on) => {
18  on("prompt.submit", async ($, e, next) => {
19    const typed = e.text.trim();
20    if (e.origin.kind !== "composer" || !typed.startsWith(ASK_PREFIX))
21      return next(e);
22
23    const text = typed.slice(ASK_PREFIX.length).trimStart();
24    if (!text) return next(e);
25    const words = text.split(/\s+/).length;
26    // A plugin's resubmission loses attachments, extra context and @file expansion, so those prompts go straight through.
27    const canResubmit =
28      !e.attachments?.length && !e.context?.length && !/(^|\s)@\S/.test(text);
29    if (text.startsWith("/") || words < MIN_WORDS || !canResubmit)
30      return next({ ...e, text });
31
32    // A hook may not wait on the person (its budget is 10s), so the prompt is held and the dialog answered after.
33    void confirm($, e.text, text);
34
35    return {
36      drop: "Held for the prompt enhancer: answer its question, or press Esc to keep editing.",
37    };
38  }).catch(($, e, next) => next(e));
39};
40
41// Asks in Claude Code's own question dialog; Esc (which rejects the ask) puts the prompt back to edit, as typed.
42async function confirm($: EngineInterface, typed: string, original: string) {
43  let answer: string;
44  try {
45    answer = await $.ui.ask(
46      `“${preview(original)}”\nPolish this prompt before it goes out?`,
47      { header: "Prompt", options: [YES, NO] },
48    );
49  } catch {
50    await $.prompt.fill({ text: typed });
51    return;
52  }
53
54  if (answer === YES) return polish($, original);
55  if (answer !== NO) {
56    // Text typed under "Type something" is not an answer to send or polish; the prompt goes back to edit.
57    await $.prompt.fill({ text: typed });
58    return;
59  }
60  await send($, original);
61}
62
63async function polish($: EngineInterface, original: string) {
64  $.ui.status("Polishing your prompt…");
65  const result = await $.model
66    .complete({
67      model: MODEL,
68      system: SYSTEM,
69      prompt: original,
70      maxTokens: 4096,
71      effort: "low",
72    })
73    .catch(() => ({
74      isAnswered: false as const,
75      reason: "request refused",
76    }));
77  $.ui.status(undefined);
78  if (result.isAnswered && result.text.trim()) {
79    const improved = result.text.trim();
80    $.ui.log(`Improved prompt:\n${improved}`);
81    // The person reviews it in the box; their own Enter sends it, as it has no prefix.
82    const filled = await $.prompt.fill({ text: improved, mode: "replace" });
83    if (!filled.isFilled) {
84      $.ui.toast(
85        "Could not put the improved prompt in the prompt box; it is in the transcript above.",
86      );
87    }
88    return;
89  }
90  $.ui.toast(
91    `Could not improve the prompt (${result.isAnswered ? "empty reply" : result.reason}); sent it as typed.`,
92  );
93  await send($, original);
94}
95
96async function send($: EngineInterface, text: string) {
97  await $.prompt.submit({ text, asUser: true });
98  // The held prompt's text comes back into the box after the drop, so it is cleared once sent.
99  await $.prompt.fill({ text: "" });
100}
101
102function preview(text: string) {
103  const flat = text.replace(/\s+/g, " ").trim();
104  return flat.length > PREVIEW_CHARS
105    ? `${flat.slice(0, PREVIEW_CHARS - 1)}…`
106    : flat;
107}
108