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…

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.
claude plugin marketplace add Reasonofmoon/bitgame-mods
claude plugin install game-spell-check@bitgame-mods
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> |
sk-…, AKIA…, ghp_…, password=…); it warns and does not stop the prompt. The trap guard stops secrets in tool calls.입력 중 모호한 말(적당히·알아서·대충…)에 밑줄, 비밀값에 빨간 표시를 하고 힌트 줄에 무엇이 되면 끝인지 적어라를 띄웁니다. 완료 조건 없이 보내면 Claude에게만 보이는 메모로 완료 조건부터 정하게 합니다. 내 메시지는 P1 · 이름 창으로 그립니다.
hooks/register.tsx 155 lines1import { 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}
155hooks/palette.ts 124 lines1// 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}
124types/index.d.ts 27 lines1/** 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