SLOPSHOPPER

GAME MODE · Spell Check

Vague words (적당히, 알아서, 대충 …) underlined while you type and secrets in red; a vague prompt with no done condition asks Claude to state one first; your messages…

newspinnerrowsprompt
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · game-spell-check
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ P1 · dev │ │ 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 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Your message
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ P1 · dev │ │ fix the failing auth test and add an audit log call │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
README

GAME MODE · Spell Check

While you type, words that leave the finish line open (적당히, 알아서, 대충, 깔끔하게, properly, somehow …) are underlined in the warning color, values that look like secrets in the error color, and the hint line says what to add:

❯ 로그인 버그 적당히 고쳐줘
  ⏸ manual mode on · ⚠ '적당히' — 무엇이 되면 끝인지 적어라

A prompt sent with such a word and no done condition (완료 조건: …, … 통과하면 끝, done when …) carries a note only Claude reads, asking it to state the done condition in one line before it starts, and to check it at the end. Your messages draw in a window:

╭──────────────────────────────────────╮
│ P1 · moon                              │
│ 로그인 버그 적당히 고쳐줘                 │
│ ⚑ 완료 조건 · Claude에게 먼저 정하라고 함   │
╰──────────────────────────────────────╯

A done condition you wrote shows as ⚑ 완료 조건 · <it>. Only prompts typed in the prompt box are touched.

Part of the GAME MODE pack. Requires Claude Code 2.1.287+; built and tested on 2.1.291.

Install

claude plugin marketplace add Reasonofmoon/bitgame-mods
claude plugin install game-spell-check@bitgame-mods

Settings

intensity (casual)off · casual · hardcore (also asks for a done condition on any prompt over 200 characters without one)
palette (nes)nes · gameboy · amber · theme
player (your login name)the name in P1 · <name>

Known limits

  • The secret check marks shapes (sk-…, AKIA…, ghp_…, password=…); it warns and does not stop the prompt. The trap guard stops secrets in tool calls.

한국어

입력 중 모호한 말(적당히·알아서·대충…)에 밑줄, 비밀값에 빨간 표시를 하고 힌트 줄에 무엇이 되면 끝인지 적어라를 띄웁니다. 완료 조건 없이 보내면 Claude에게만 보이는 메모로 완료 조건부터 정하게 합니다. 내 메시지는 P1 · 이름 창으로 그립니다.

Source 3 files
hooks/register.tsx 155 lines
1import { atom, read, update } from 'claude-code'
2import type { PromptDecoration, Register } from 'claude-code'
3
4import type { Draft, Sent } from '../types'
5import { intensityOf, paletteOf } from './palette'
6
7// GAME MODE · SPELL CHECK
8//
9// While you type, words that leave the finish line open (적당히, 알아서, 대충, 깔끔하게 …)
10// are underlined in the warning color, values that look like secrets in the error color,
11// and the hint line says what to add:
12//   ⚠ '적당히' — 무엇이 되면 끝인지 적어라
13// A prompt sent with such a word and no done condition carries a note only Claude reads,
14// asking it to state the done condition before it starts. Your messages draw in a window:
15//   ╭ P1 · moon ─────────────────────────╮
16//   │ 로그인 버그 적당히 고쳐줘             │
17//   │ ⚑ 완료 조건 · Claude에게 먼저 정하라고 함 │
18
19type Found = { word: string; start: number; end: number }
20
21const VAGUE = [
22  '적당히',
23  '알아서',
24  '대충',
25  '깔끔하게',
26  '대강',
27  '적절히',
28  '예쁘게',
29  '그럴듯하게',
30  '웬만하면',
31  '잘 좀',
32  '좋게 좀',
33  'properly',
34  'nicely',
35  'somehow',
36  'appropriately',
37  'as needed',
38  'clean it up',
39  'make it better',
40]
41const VAGUE_RE = new RegExp(VAGUE.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'gi')
42
43// Values that look like credentials. Shapes only: no real key is kept here.
44const SECRETS: readonly RegExp[] = [
45  /\bsk-(?:ant-)?[A-Za-z0-9_-]{16,}/g,
46  /\bAKIA[0-9A-Z]{16}\b/g,
47  /\bgh[pousr]_[A-Za-z0-9]{30,}/g,
48  /\bxox[abpr]-[A-Za-z0-9-]{10,}/g,
49  /\bAIza[0-9A-Za-z_-]{35}\b/g,
50  /-----BEGIN [A-Z ]*PRIVATE KEY-----/g,
51  /\b(?:password|passwd|pwd|secret|token|api[_-]?key)\s*[:=]\s*\S{6,}/gi,
52]
53
54// A done condition the person wrote: a line naming it, or a sentence that says when it ends.
55const DONE_LINE = /^\s*(?:완료\s*조건|끝\s*조건|done\s*when|definition\s+of\s+done|acceptance)\s*[::]\s*(.+)$/im
56const DONE_SENTENCE = /[^.!?\n]*(?:통과하면|되면\s*끝|하면\s*끝|까지만|나오면\s*끝|done when|until [^.!?\n]+ pass(?:es)?)[^.!?\n]*/i
57
58const EMPTY: Draft = { vague: '', hasSecret: false }
59const draft = atom({ plugin: 'game-spell-check', key: 'draft' } as const, EMPTY)
60const sent = atom({ plugin: 'game-spell-check', key: 'sent' } as const, [] as Sent[])
61
62export const register: Register = (on, options) => {
63  const intensity = intensityOf(options.intensity)
64  const pal = paletteOf(options.palette)
65  let player = typeof options.player === 'string' ? options.player.trim() : ''
66
67  on('session.start', async ($, e, next) => {
68    if (player === '') player = (await $.env.get('USER')) ?? (await $.env.get('USERNAME')) ?? (await $.env.get('LOGNAME')) ?? 'PLAYER'
69    return next(e)
70  })
71
72  on('prompt.edit', async ($, e, next) => {
73    const box = await next(e)
74    if (intensity === 'off') return box
75    const vague = vagueIn(box.text)
76    const secrets = secretsIn(box.text)
77    const now: Draft = { vague: vague[0]?.word ?? '', hasSecret: secrets.length > 0 }
78    const was = await read($, draft)
79    if (was.vague !== now.vague || was.hasSecret !== now.hasSecret) await update($, draft, () => now)
80    if (vague.length === 0 && secrets.length === 0) return box
81    const marks: PromptDecoration[] = [
82      ...vague.map(f => ({ start: f.start, end: f.end, color: pal.warn, underline: true })),
83      ...secrets.map(f => ({ start: f.start, end: f.end, color: pal.bad, underline: true, bold: true })),
84    ]
85    return { ...box, decorations: [...(box.decorations ?? []), ...marks] }
86  }).catch(($, e, next) => next(e))
87
88  on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
89    if (intensity === 'off' || !e.props.isDraft) return next(e)
90    const d = await read($, draft)
91    const mine = d.hasSecret ? '⚠ 비밀 값이 보인다 — 보내기 전에 지워라' : d.vague !== '' ? `⚠ '${d.vague}' — 무엇이 되면 끝인지 적어라` : ''
92    if (mine === '') return next(e)
93    const tail = e.props.tail === undefined || e.props.tail === '' ? mine : `${e.props.tail} · ${mine}`
94    return next({ ...e, props: { ...e.props, tail } })
95  })
96
97  on('prompt.submit', async ($, e, next) => {
98    if (intensity === 'off' || e.origin.kind !== 'composer') return next(e)
99    const vague = vagueIn(e.text).map(f => f.word)
100    const done = doneOf(e.text)
101    const isLong = intensity === 'hardcore' && e.text.length > 200
102    const isAsked = done === '' && (vague.length > 0 || isLong)
103    await update($, draft, () => EMPTY)
104    await update($, sent, list => [...list, { text: e.text, done, isAsked }].slice(-50))
105    if (!isAsked) return next(e)
106    return next({ ...e, context: [...(e.context ?? []), noteFor(vague)] })
107  }).catch(($, e, next) => next(e))
108
109  on('ui.render', { component: 'UserMessage' }, async ($, e, next) => {
110    if (intensity === 'off' || e.props.origin.kind !== 'composer') return next(e)
111    const list = await read($, sent)
112    const s = [...list].reverse().find(x => x.text === e.props.text)
113    const { Box, Text } = $.ui.resolve(e)
114    return (
115      <Box flexDirection="column" borderStyle="round" borderColor={pal.frame} paddingX={1}>
116        <Text color={pal.title} bold>
117          {`P1 · ${player}`}
118        </Text>
119        <Text color={pal.text}>{e.props.text}</Text>
120        {s !== undefined && s.done !== '' && <Text color={pal.ok}>{`⚑ 완료 조건 · ${s.done}`}</Text>}
121        {s !== undefined && s.isAsked && <Text color={pal.warn}>⚑ 완료 조건 · Claude에게 먼저 정하라고 함</Text>}
122      </Box>
123    )
124  })
125}
126
127export function vagueIn(text: string): Found[] {
128  return [...text.matchAll(VAGUE_RE)].map(m => ({ word: m[0], start: m.index ?? 0, end: (m.index ?? 0) + m[0].length }))
129}
130
131export function secretsIn(text: string): Found[] {
132  const out: Found[] = []
133  for (const re of SECRETS) {
134    for (const m of text.matchAll(re)) out.push({ word: m[0], start: m.index ?? 0, end: (m.index ?? 0) + m[0].length })
135  }
136  return out.sort((a, b) => a.start - b.start)
137}
138
139/** The done condition the prompt states, as written; '' when it states none. */
140export function doneOf(text: string): string {
141  const line = DONE_LINE.exec(text)
142  if (line?.[1] !== undefined) return line[1].trim()
143  return DONE_SENTENCE.exec(text)?.[0].trim() ?? ''
144}
145
146export function noteFor(vague: readonly string[]): string {
147  const words = vague.length > 0 ? `모호한 표현(${[...new Set(vague)].map(w => `'${w}'`).join(', ')})이 있고 ` : ''
148  return [
149    '[GAME MODE · Spell Check] 이 메모는 사용자에게 보이지 않습니다.',
150    `사용자의 요청에 ${words}완료 조건이 적혀 있지 않습니다.`,
151    '작업을 시작하기 전에 무엇이 되면 끝인지(완료 조건)를 한 줄로 먼저 밝히고, 그 기준으로 진행한 뒤 끝에서 그 조건을 확인하세요.',
152    '합리적인 기준을 정할 수 없으면 사용자에게 먼저 물어보세요.',
153  ].join('\n')
154}
155
hooks/palette.ts 124 lines
1// GAME MODE shared look (copied into each GAME MODE plugin: a hooks module
2// may only import files of its own plugin). Change every copy together;
3// scripts/check.sh fails when the copies differ.
4//
5// nes / gameboy / amber are raw colors made for dark terminals, taken from
6// the design boards. `theme` uses Claude Code's theme keys, so it follows the
7// person's theme: pick it on a light background. Its `win` is empty: windows
8// draw with no fill there.
9
10export type PaletteName = 'nes' | 'gameboy' | 'amber' | 'theme'
11
12export type Palette = {
13  /** Headings, gold, the level. */
14  title: string
15  ok: string
16  warn: string
17  bad: string
18  info: string
19  dim: string
20  text: string
21  /** A window's border. */
22  frame: string
23  /** A window's fill; '' for none. */
24  win: string
25  /** The bright inner edge of a window, and a highlighted badge's text. */
26  edge: string
27  /** A bar's empty cells. */
28  off: string
29}
30
31const PALETTES: Record<PaletteName, Palette> = {
32  nes: {
33    title: '#f8d830',
34    ok: '#58d854',
35    warn: '#ff8a6a',
36    bad: '#f83800',
37    info: '#3cbcfc',
38    dim: '#b8b8f0',
39    text: '#fcfcfc',
40    frame: '#5060d8',
41    win: '#1c2a8a',
42    edge: '#fcfcfc',
43    off: '#2c3a98',
44  },
45  gameboy: {
46    title: '#e0f8d0',
47    ok: '#9bbc0f',
48    warn: '#e0f8d0',
49    bad: '#f8f8a0',
50    info: '#c4e890',
51    dim: '#8bac0f',
52    text: '#e0f8d0',
53    frame: '#9bbc0f',
54    win: '#1e4a1e',
55    edge: '#e0f8d0',
56    off: '#306230',
57  },
58  amber: {
59    title: '#ffd060',
60    ok: '#ffb000',
61    warn: '#ff7040',
62    bad: '#ff5a2a',
63    info: '#ffe0a0',
64    dim: '#c89040',
65    text: '#ffd9a0',
66    frame: '#ffb000',
67    win: '#2a1700',
68    edge: '#ffe0a0',
69    off: '#4a2a00',
70  },
71  theme: {
72    title: 'claude',
73    ok: 'success',
74    warn: 'warning',
75    bad: 'error',
76    info: 'suggestion',
77    dim: 'subtle',
78    text: 'text',
79    frame: 'promptBorder',
80    win: '',
81    edge: 'text',
82    off: 'subtle',
83  },
84}
85
86export function paletteOf(name: unknown): Palette {
87  return name === 'gameboy' || name === 'amber' || name === 'theme' ? PALETTES[name] : PALETTES.nes
88}
89
90export type Intensity = 'off' | 'casual' | 'hardcore'
91
92export function intensityOf(value: unknown): Intensity {
93  return value === 'off' || value === 'hardcore' ? value : 'casual'
94}
95
96/** A bar of `cells` blocks, `percent` of them full. */
97export function bar(percent: number, cells: number): string {
98  const full = Math.max(0, Math.min(cells, Math.round((percent / 100) * cells)))
99  return '█'.repeat(full) + '░'.repeat(cells - full)
100}
101
102/** Box props for a window in this palette: a double border and, where the palette has one, a fill. */
103export function windowProps(pal: Palette): { borderStyle: 'double'; borderColor: string; backgroundColor?: string; paddingX: number } {
104  return pal.win === ''
105    ? { borderStyle: 'double', borderColor: pal.frame, paddingX: 1 }
106    : { borderStyle: 'double', borderColor: pal.frame, backgroundColor: pal.win, paddingX: 1 }
107}
108
109/** `m:ss`, or `h:mm:ss` past an hour. */
110export function clock(ms: number): string {
111  const s = Math.max(0, Math.round(ms / 1000))
112  const h = Math.floor(s / 3600)
113  const m = Math.floor((s % 3600) / 60)
114  const sec = s % 60
115  const two = (n: number) => String(n).padStart(2, '0')
116  return h > 0 ? `${h}:${two(m)}:${two(sec)}` : `${m}:${two(sec)}`
117}
118
119/** A cost in the chosen currency: `$1.23`, or `₩1,722` at `krwPerUsd`. */
120export function money(usd: number, currency: 'usd' | 'krw', krwPerUsd: number): string {
121  if (currency === 'krw') return '₩' + String(Math.round(usd * krwPerUsd)).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
122  return '$' + usd.toFixed(2)
123}
124
types/index.d.ts 27 lines
1/** What the draft in the prompt box holds now. */
2export type Draft = {
3  /** The first vague word in it, as typed; '' when none. */
4  vague: string
5  /** A value that looks like a secret is in it. */
6  hasSecret: boolean
7}
8
9/** A prompt the person sent, as its message window shows it. */
10export type Sent = {
11  text: string
12  /** The done condition the person wrote; '' when none. */
13  done: string
14  /** The note asking Claude to state a done condition first went with it. */
15  isAsked: boolean
16}
17
18declare module 'claude-code' {
19  interface PluginState {
20    'game-spell-check': {
21      draft: Draft
22      /** The last 50 prompts sent from the prompt box. */
23      sent: Sent[]
24    }
25  }
26}
27