SLOPSHOPPER

Launch Codes

Dangerous Bash commands need launch codes: red alert pane, siren, a code to arm and a LAUNCH to fire

newpaneguardcommandtimeraudio
★ 2v0.1.0MITupdated 2026-10-02OneWave-AI/claude-code-mods/launch-codes
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · launch-codes
│ ┃ launch-codes ✕ › fix the failing auth test and add an audit log call │ ┃ All quiet. Dangerous commands will raise │ ┃ the alarm here. ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(rm -rf build && git push --force origin main) │ ⎿ Denied by launch-codes: launch-codes: blocked `rm -rf bui │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /launch-codes │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · launch-codes
All quiet. Dangerous commands will raise the alarm here.
README

launch-codes

Dangerous Bash commands need launch codes: red alert pane, siren, a code to arm and a LAUNCH to fire.

launch-codes

Hooks tool.call on Bash and classifies the command before it runs: risky rm -rf, git push --force, git reset --hard, DROP/TRUNCATE through a live SQL client, supabase db reset, vercel --prod, chmod -R 777, and downloaded scripts piped into a shell. A match opens a red-alert pane with a siren and a one-time code. Type the code to arm, press LAUNCH to fire. Anything else is denied and Claude is told why. /launch-codes test <command> shows the verdict without running anything.

It is strict. It blocked one of our own cleanup commands while we were putting this repo together, which is the point.

Install

In a Claude Code session (2.1.287 or later):

/plugin marketplace add OneWave-AI/claude-code-mods
/plugin install launch-codes@claude-code-mods

Or load this folder for one session: claude --plugin-dir ./launch-codes

What it reaches

ModNetworkRuns processesFilesCalls a modelSends data anywhere
launch-codesNoNoNoNoNo

Run claude plugin validate ./launch-codes to list every event it hooks and every call it makes. See the repository README for the full security notes.

Part of claude-code-mods by OneWave AI. MIT licensed.

Source 4 files
hooks/register.tsx 215 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Alert, Phase } from '../types'
5import { classify, isCode, makeCode } from './classify'
6import { hazardCells, launchWav, sirenWav } from './fx'
7
8const PANE = 'launch-codes'
9const CODE_TTL_MS = 30_000
10const HAZARD_ROWS = 2
11
12const alert = atom({ plugin: 'launch-codes', key: 'alert' } as const, null as Alert | null)
13
14const RED = '#e5484d'
15const GOLD = '#c2a87e'
16const PAPER = '#E8E2D6'
17const INK = '#0a0606'
18
19const SIREN = sirenWav()
20const LAUNCH = launchWav()
21
22let hazardTimer: { cancel: () => void } | null = null
23let hazardSize = 0
24
25const startHazard = ($: EngineInterface, columns: number) => {
26  if (hazardTimer && hazardSize === columns) return
27  hazardTimer?.cancel()
28  hazardSize = columns
29  const timer = $.clock.every(70, () => {
30    void Promise.all([$.clock.now(), read($, alert)]).then(([now, a]) =>
31      Promise.all(
32        ['top', 'bottom'].map(key =>
33          $.ui.blit({
34            requestId: PANE,
35            key,
36            cells: hazardCells(columns, HAZARD_ROWS, now / 1000, a?.phase === 'armed' || a?.phase === 'launched'),
37          }),
38        ),
39      ).then(results => {
40        if (results.some(r => r.deny) && hazardTimer === timer) {
41          timer.cancel()
42          hazardTimer = null
43        }
44      }),
45    )
46  })
47  hazardTimer = timer
48}
49
50const random = (n: number) => {
51  const box = new Uint32Array(1)
52  crypto.getRandomValues(box)
53  return box[0]! % n
54}
55
56const setPhase = ($: EngineInterface, phase: Phase) =>
57  update($, alert, a => (a ? { ...a, phase } : a))
58
59const short = (command: string) => (command.length > 70 ? `${command.slice(0, 67)}...` : command)
60
61/**
62 * Runs the whole ceremony for one command and says whether it may fire.
63 * Every wait is a `$` call (the ask dialog), so none of it spends the hook's budget.
64 */
65const ceremony = async ($: EngineInterface, command: string, reason: string, isDemo: boolean) => {
66  const now = await $.clock.now()
67  const code = makeCode(random)
68  await update($, alert, () => ({
69    command,
70    reason,
71    code,
72    startedAt: now,
73    expiresAt: now + CODE_TTL_MS,
74    phase: 'code' as Phase,
75    isDemo,
76  }))
77  await $.ui.open({ id: PANE, title: 'RED ALERT', rows: 16 })
78  const siren = new AbortController()
79  void $.audio.play({ base64: SIREN, mime: 'audio/wav' }, { shouldLoop: true, signal: siren.signal, gain: 0.6 }).catch(() => undefined)
80  // Repaint once a second for the countdown.
81  const tick = $.clock.every(1000, () => $.ui.invalidate('ui.render'))
82
83  const finish = async (phase: Phase, verdict: { isLaunch: boolean; why: string }) => {
84    siren.abort()
85    tick.cancel()
86    await setPhase($, phase)
87    if (phase === 'launched') void $.audio.play({ base64: LAUNCH, mime: 'audio/wav' }).catch(() => undefined)
88    $.clock.after(phase === 'launched' ? 1800 : 2500, () => {
89      void $.ui.close({ id: PANE }).then(() => update($, alert, () => null))
90    })
91    return verdict
92  }
93
94  try {
95    const typed = await $.ui.ask(
96      `Launch codes required: ${short(command)}. Type the 4-character code from the RED ALERT pane under "Other" to arm it.`,
97      { header: 'LAUNCH CODE', options: ['ABORT', 'ABORT and find a safer way'] },
98    )
99    if (!isCode(typed, code)) {
100      const why = typed.startsWith('ABORT') ? 'the user pressed ABORT' : 'the launch code was wrong'
101      return finish('aborted', { isLaunch: false, why })
102    }
103    if ((await $.clock.now()) > now + CODE_TTL_MS) {
104      return finish('aborted', { isLaunch: false, why: 'the launch code expired before it was entered' })
105    }
106    siren.abort()
107    await setPhase($, 'armed')
108    const fire = await $.ui.ask(`ARMED. Fire ${short(command)}?`, {
109      header: 'ARMED',
110      options: ['LAUNCH', 'ABORT'],
111    })
112    if (fire !== 'LAUNCH') return finish('aborted', { isLaunch: false, why: 'the user pressed ABORT after arming' })
113    return finish('launched', { isLaunch: true, why: 'launched' })
114  } catch {
115    return finish('aborted', { isLaunch: false, why: 'no one was there to enter the launch code' })
116  }
117}
118
119const deny = (command: string, reason: string, why: string) => ({
120  deny:
121    `launch-codes: blocked \`${short(command)}\` (${reason}) because ${why}. ` +
122    'Do not retry it or work around the check; tell the user what you wanted to do and ask, or find a safer way.',
123})
124
125export const register: Register = on => {
126  on('session.start', async ($, e, next) => {
127    await $.command.register({
128      name: 'launch-codes',
129      description: 'Launch codes for dangerous commands: /launch-codes [demo|test <command>]',
130    })
131    return next(e)
132  })
133
134  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
135    const danger = classify(e.command)
136    if (!danger) return next(e)
137    const verdict = await ceremony($, e.command, danger.reason, false)
138    return verdict.isLaunch ? next(e) : deny(e.command, danger.reason, verdict.why)
139  })
140    // A hook that fails must not let the command through.
141    .catch(($, e) => {
142      const danger = classify(e.command)
143      return danger ? deny(e.command, danger.reason, 'the launch check failed') : { deny: 'launch-codes: the launch check failed; ask the user before retrying.' }
144    })
145
146  on('command.run', { command: 'launch-codes' }, async ($, e) => {
147    const arg = e.args.trim()
148    if (arg.startsWith('test ')) {
149      const danger = classify(arg.slice(5))
150      return { text: danger ? `Needs launch codes: ${danger.reason}.` : 'Safe: runs without launch codes.' }
151    }
152    const command = 'rm -rf ~/Projects/prod-backup && git push --force origin main'
153    const verdict = await ceremony($, command, 'rm -rf on ~/Projects/prod-backup', true)
154    return { text: verdict.isLaunch ? 'Demo: LAUNCHED. (Nothing ran, it was a demo.)' : `Demo: aborted, ${verdict.why}. Nothing ran.` }
155  })
156
157  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
158    const elements = $.ui.resolve(e)
159    const { Box, Text } = elements
160    const Raster = 'Raster' in elements ? elements.Raster : null
161    const a = await read($, alert)
162    const now = await $.clock.now()
163    const width = Math.max(30, (e.props.bodyColumns || e.viewport?.columns || 80) - 2)
164    if (!a) {
165      return (
166        <Box paddingX={1}>
167          <Text dimColor>All quiet. Dangerous commands will raise the alarm here.</Text>
168        </Box>
169      )
170    }
171
172    const isTerminal = e.surface === 'terminal' && Raster !== null
173    if (isTerminal) startHazard($, width)
174    const left = Math.max(0, Math.ceil((a.expiresAt - now) / 1000))
175    const isFlash = Math.floor(now / 400) % 2 === 0
176    const banner: Record<Phase, { text: string; color: string }> = {
177      code: { text: 'LAUNCH CODES REQUIRED', color: RED },
178      armed: { text: 'ARMED  -  CONFIRM LAUNCH', color: GOLD },
179      launched: { text: 'LAUNCHED', color: GOLD },
180      aborted: { text: 'ABORTED  -  NOTHING RAN', color: PAPER },
181    }
182    const head = banner[a.phase]
183    const hazard = (key: string) =>
184      isTerminal && Raster ? (
185        <Raster key={key} columns={width} rows={HAZARD_ROWS} cells={hazardCells(width, HAZARD_ROWS, now / 1000, a.phase !== 'code' && a.phase !== 'aborted')} />
186      ) : (
187        <Text color={RED}>{'/'.repeat(width)}</Text>
188      )
189
190    return (
191      <Box flexDirection="column" paddingX={1}>
192        {hazard('top')}
193        <Box justifyContent="space-between" marginTop={1}>
194          <Text bold backgroundColor={head.color} color={INK}>{` ${head.text} `}</Text>
195          <Text dimColor>{a.isDemo ? 'DEMO  ' : ''}{a.phase === 'code' ? `code expires in ${left}s` : ''}</Text>
196        </Box>
197        <Box marginTop={1} flexDirection="column">
198          <Text dimColor>COMMAND</Text>
199          <Text bold color={PAPER} wrap="wrap">{a.command}</Text>
200          <Text color={RED}>{a.reason}</Text>
201        </Box>
202        {a.phase === 'code' && (
203          <Box marginTop={1} gap={2}>
204            <Text dimColor>LAUNCH CODE</Text>
205            <Text bold color={isFlash ? GOLD : PAPER}>{a.code.split('').join('  ')}</Text>
206          </Box>
207        )}
208        {a.phase === 'code' && <Text dimColor>Type it under "Other" in the dialog below, then confirm LAUNCH. ABORT blocks it.</Text>}
209        {a.phase === 'armed' && <Text color={GOLD} bold>Code accepted. Choose LAUNCH to fire or ABORT to stand down.</Text>}
210        <Box marginTop={1}>{hazard('bottom')}</Box>
211      </Box>
212    )
213  })
214}
215
hooks/classify.ts 210 lines
1/** Which shell commands need launch codes. Pure: a command string in, a reason (or null) out. */
2
3export type Danger = { kind: string; reason: string }
4
5type Token = { op: string } | { word: string }
6
7/** Splits a command line into words and operators, honoring quotes and backslashes. */
8export const tokenize = (line: string): Token[] => {
9  const out: Token[] = []
10  let word = ''
11  let hasWord = false
12  const flush = () => {
13    if (hasWord) out.push({ word })
14    word = ''
15    hasWord = false
16  }
17  for (let i = 0; i < line.length; i++) {
18    const c = line[i]!
19    if (c === "'") {
20      const end = line.indexOf("'", i + 1)
21      word += line.slice(i + 1, end === -1 ? line.length : end)
22      hasWord = true
23      i = end === -1 ? line.length : end
24    } else if (c === '"') {
25      let j = i + 1
26      while (j < line.length && line[j] !== '"') {
27        if (line[j] === '\\' && j + 1 < line.length) j++
28        word += line[j]
29        j++
30      }
31      hasWord = true
32      i = j
33    } else if (c === '\\' && i + 1 < line.length) {
34      word += line[i + 1]
35      hasWord = true
36      i++
37    } else if (c === ' ' || c === '\t') {
38      flush()
39    } else if (c === '\n' || c === ';') {
40      flush()
41      out.push({ op: ';' })
42    } else if (c === '&' || c === '|') {
43      flush()
44      const pair = line[i + 1] === c
45      out.push({ op: pair ? c + c : c })
46      if (pair) i++
47    } else if (c === '(' || c === ')') {
48      flush()
49      out.push({ op: c })
50    } else {
51      word += c
52      hasWord = true
53    }
54  }
55  flush()
56  return out
57}
58
59/** Simple commands, each with the pipe position it sits in. */
60type Simple = { words: string[]; pipedFrom: string | null }
61
62const WRAPPERS = new Set(['sudo', 'time', 'command', 'exec', 'nohup', 'env', 'npx', 'bunx', 'xargs', 'doas'])
63
64/** The program a command really runs, past sudo, env assignments and npx. */
65const program = (words: string[]) => {
66  let i = 0
67  while (i < words.length) {
68    const w = words[i]!
69    if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(w) || WRAPPERS.has(w) || (w.startsWith('-') && i > 0)) {
70      i++
71      continue
72    }
73    if ((w === 'pnpm' || w === 'yarn' || w === 'bun') && words[i + 1] === 'dlx') {
74      i += 2
75      continue
76    }
77    break
78  }
79  return { name: (words[i] ?? '').split('/').pop() ?? '', args: words.slice(i + 1) }
80}
81
82const simples = (tokens: Token[]): Simple[] => {
83  const out: Simple[] = []
84  let words: string[] = []
85  let pipedFrom: string | null = null
86  const end = (op: string) => {
87    if (words.length) {
88      const name = program(words).name
89      out.push({ words, pipedFrom })
90      pipedFrom = op === '|' ? name : null
91    } else if (op !== '|') {
92      pipedFrom = null
93    }
94    words = []
95  }
96  for (const t of tokens) {
97    if ('op' in t) end(t.op)
98    else words.push(t.word)
99  }
100  end(';')
101  return out
102}
103
104const SAFE_RM = /(^|\/)(node_modules|\.next|dist|tmp|\.turbo|\.cache|coverage|__pycache__)(\/|$)/
105const SAFE_RM_ROOT = /^(\/tmp\/|\/private\/tmp\/|\$TMPDIR|\/var\/folders\/)/
106
107const isSafeRmTarget = (path: string) => SAFE_RM.test(path) || SAFE_RM_ROOT.test(path)
108
109const rmDanger = (args: string[]): Danger | null => {
110  let recursive = false
111  let force = false
112  const targets: string[] = []
113  let isEndOfFlags = false
114  for (const a of args) {
115    if (!isEndOfFlags && a === '--') {
116      isEndOfFlags = true
117    } else if (!isEndOfFlags && a.startsWith('--')) {
118      if (a === '--recursive') recursive = true
119      if (a === '--force') force = true
120    } else if (!isEndOfFlags && a.startsWith('-') && a.length > 1) {
121      if (/[rR]/.test(a)) recursive = true
122      if (/f/.test(a)) force = true
123    } else {
124      targets.push(a)
125    }
126  }
127  if (!recursive || !force || targets.length === 0) return null
128  const risky = targets.filter(t => !isSafeRmTarget(t))
129  if (risky.length === 0) return null
130  return { kind: 'rm', reason: `rm -rf on ${risky.slice(0, 3).join(' ')}` }
131}
132
133const SQL_CLIENTS = new Set(['psql', 'mysql', 'mariadb', 'sqlite3', 'sqlcmd', 'duckdb', 'clickhouse-client', 'pgcli', 'mycli'])
134const SQL_DROP = /\b(drop\s+(table|database|schema)|truncate\s+(table\s+)?["`\w])/i
135const SHELLS = new Set(['sh', 'bash', 'zsh', 'dash', 'fish'])
136const FETCHERS = new Set(['curl', 'wget'])
137
138/** The first dangerous thing `command` does, or null when it is safe to run unasked. */
139export const classify = (command: string): Danger | null => {
140  const tokens = tokenize(command)
141  const list = simples(tokens)
142  for (const s of list) {
143    const { name, args } = program(s.words)
144    const joined = args.join(' ')
145
146    if (name === 'rm') {
147      const d = rmDanger(args)
148      if (d) return d
149    }
150
151    if (name === 'git') {
152      // git's own options come before the subcommand; -C and -c take a value.
153      let at = 0
154      while (at < args.length && args[at]!.startsWith('-')) at += ['-C', '-c', '--git-dir', '--work-tree'].includes(args[at]!) ? 2 : 1
155      const sub = args[at]
156      if (sub === 'push') {
157        const rest = args.slice(at + 1)
158        const isForce = rest.some(a => a === '--force' || /^-[a-zA-Z]*f[a-zA-Z]*$/.test(a) || /^\+\S/.test(a))
159        if (isForce) return { kind: 'git-push-force', reason: 'git push --force rewrites remote history' }
160      }
161      if (sub === 'reset' && args.includes('--hard')) {
162        return { kind: 'git-reset-hard', reason: 'git reset --hard throws away uncommitted work' }
163      }
164    }
165
166    // A heredoc's body lands in commands of its own, so then the whole line counts.
167    const sql = args.some(a => a.startsWith('<<')) ? command : joined
168    if (SQL_CLIENTS.has(name) && SQL_DROP.test(sql)) {
169      return { kind: 'sql-drop', reason: 'DROP / TRUNCATE through a live SQL client' }
170    }
171
172    if (name === 'supabase') {
173      if (args[0] === 'db' && args[1] === 'reset') {
174        return { kind: 'supabase-reset', reason: 'supabase db reset wipes the database' }
175      }
176      if (SQL_DROP.test(joined)) return { kind: 'sql-drop', reason: 'DROP / TRUNCATE through supabase' }
177    }
178
179    if (name === 'vercel' && args.some(a => a === '--prod' || a === '--production')) {
180      return { kind: 'vercel-prod', reason: 'vercel --prod ships to production' }
181    }
182
183    if (name === 'chmod' && args.some(a => /^-[a-zA-Z]*R/.test(a) || a === '--recursive') && args.some(a => /^0?777$/.test(a) || a === 'a+rwx')) {
184      return { kind: 'chmod-777', reason: 'chmod -R 777 opens every file to everyone' }
185    }
186
187    if (SHELLS.has(name) && s.pipedFrom && FETCHERS.has(s.pipedFrom)) {
188      return { kind: 'curl-sh', reason: `${s.pipedFrom} piped straight into ${name}` }
189    }
190    if (SHELLS.has(name) && args.some(a => /\$\(\s*(curl|wget)\b/.test(a))) {
191      return { kind: 'curl-sh', reason: `${name} running a downloaded script` }
192    }
193  }
194  // bash <(curl ...): the tokenizer splits the parens off, so look at the raw line.
195  if (/\b(sh|bash|zsh)\s+<\(\s*(curl|wget)\b/.test(command)) {
196    return { kind: 'curl-sh', reason: 'shell running a downloaded script' }
197  }
198  return null
199}
200
201const CODE_ALPHABET = 'ACDEFHJKMNPRTVWXY3479'
202
203/** A 4-character launch code from unambiguous letters and digits. */
204export const makeCode = (random: (n: number) => number) =>
205  Array.from({ length: 4 }, () => CODE_ALPHABET[random(CODE_ALPHABET.length)]!).join('')
206
207/** Whether what the person typed matches the code: case and spaces don't matter. */
208export const isCode = (typed: string, code: string) =>
209  typed.replace(/[\s-]/g, '').toUpperCase() === code
210
hooks/fx.ts 102 lines
1/** The red alert's sound and pixels: a synthesized siren WAV and hazard-stripe Raster frames. */
2
3const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
4
5export const toBase64 = (bytes: Uint8Array) => {
6  let out = ''
7  for (let i = 0; i < bytes.length; i += 3) {
8    const a = bytes[i] ?? 0
9    const b = bytes[i + 1] ?? 0
10    const c = bytes[i + 2] ?? 0
11    const n = (a << 16) | (b << 8) | c
12    out += B64[(n >> 18) & 63]! + B64[(n >> 12) & 63]!
13    out += i + 1 < bytes.length ? B64[(n >> 6) & 63]! : '='
14    out += i + 2 < bytes.length ? B64[n & 63]! : '='
15  }
16  return out
17}
18
19const RATE = 11025
20
21/** A mono 8-bit WAV around `samples` (each -1..1). */
22export const wav = (samples: Float32Array) => {
23  const bytes = new Uint8Array(44 + samples.length)
24  const view = new DataView(bytes.buffer)
25  const text = (at: number, s: string) => [...s].forEach((ch, i) => view.setUint8(at + i, ch.charCodeAt(0)))
26  text(0, 'RIFF')
27  view.setUint32(4, 36 + samples.length, true)
28  text(8, 'WAVE')
29  text(12, 'fmt ')
30  view.setUint32(16, 16, true)
31  view.setUint16(20, 1, true)
32  view.setUint16(22, 1, true)
33  view.setUint32(24, RATE, true)
34  view.setUint32(28, RATE, true)
35  view.setUint16(32, 1, true)
36  view.setUint16(34, 8, true)
37  text(36, 'data')
38  view.setUint32(40, samples.length, true)
39  for (let i = 0; i < samples.length; i++) {
40    bytes[44 + i] = Math.max(0, Math.min(255, Math.round(128 + samples[i]! * 110)))
41  }
42  return bytes
43}
44
45/** One wail of a siren: a sweep up and back down, 1.4 s, square-ish so it cuts through. */
46export const sirenWav = () => {
47  const seconds = 1.4
48  const n = Math.round(RATE * seconds)
49  const out = new Float32Array(n)
50  let phase = 0
51  for (let i = 0; i < n; i++) {
52    const t = i / n
53    const sweep = t < 0.5 ? t * 2 : (1 - t) * 2
54    const freq = 620 + 760 * sweep
55    phase += (2 * Math.PI * freq) / RATE
56    const tone = Math.tanh(Math.sin(phase) * 3) * 0.7 + Math.sin(phase * 2) * 0.15
57    out[i] = tone * 0.8
58  }
59  return toBase64(wav(out))
60}
61
62/** A short rising confirm chirp: three steps, for LAUNCH. */
63export const launchWav = () => {
64  const steps = [523, 784, 1046]
65  const each = Math.round(RATE * 0.12)
66  const out = new Float32Array(each * steps.length)
67  steps.forEach((freq, s) => {
68    for (let i = 0; i < each; i++) {
69      const env = Math.min(1, i / 60) * Math.min(1, (each - i) / 200)
70      out[s * each + i] = Math.sin((2 * Math.PI * freq * i) / RATE) * 0.7 * env
71    }
72  })
73  return toBase64(wav(out))
74}
75
76const DEFAULT = 0x01000000
77const RED = 0xe5484d
78const DEEP = 0x5a0f12
79const BLACK = 0x0a0606
80const GOLD = 0xc2a87e
81
82/**
83 * A hazard band `columns` x `rows`: diagonal red/black stripes marching at
84 * time `t` (seconds), the whole band flashing between bright and deep red.
85 */
86export const hazardCells = (columns: number, rows: number, t: number, isArmed = false) => {
87  const words = new Uint32Array(columns * rows * 3)
88  const flash = Math.floor(t * 2.5) % 2 === 0
89  const hot = isArmed ? GOLD : flash ? RED : DEEP
90  const shift = Math.floor(t * 10)
91  for (let r = 0; r < rows; r++) {
92    for (let x = 0; x < columns; x++) {
93      const i = (r * columns + x) * 3
94      const stripe = Math.floor((x + r + shift) / 3) % 2 === 0
95      words[i] = 0x2588
96      words[i + 1] = stripe ? hot : BLACK
97      words[i + 2] = DEFAULT
98    }
99  }
100  return toBase64(new Uint8Array(words.buffer))
101}
102
types/index.d.ts 18 lines
1export type Phase = 'code' | 'armed' | 'launched' | 'aborted'
2
3export type Alert = {
4  command: string
5  reason: string
6  code: string
7  startedAt: number
8  expiresAt: number
9  phase: Phase
10  isDemo: boolean
11}
12
13declare module 'claude-code' {
14  interface PluginState {
15    'launch-codes': { alert: Alert | null }
16  }
17}
18