SLOPSHOPPER

snake

Snake in a pane while Claude works, paused when it's done. Opt-in, so nothing opens until /snake.

newpanespinnercommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · snake
│ ┃ Snake ✕ › fix the failing auth test and add an audit log call │ ┃ ▣ client module ./game.tsx │ ⏺ 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 │ │ › /snake │ ⎿ snake: Snake is on. It plays while Claude works. /snake again tu │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Snake
▣ client module ./game.tsx
README

claude-code-mods-collection

A private reference collection of public Claude Code mods, with a security-oriented analysis.

  • ANALYSIS.md: what each mod can reach, which to install, which to avoid
  • SOURCES.md: upstream repo and pinned commit for every folder in mods/

Nothing here is installed or executed. All code is by its upstream authors under their own licences (MIT, one Apache-2.0); each folder keeps its original LICENSE.

Source 4 files
hooks/register.tsx 95 lines
1// Snake: play in a pane while Claude works; it pauses when Claude is done.
2// Nothing opens until /snake; /snake again (or /snake stop) turns it off.
3import { atom, read, update } from 'claude-code'
4import type { Register } from 'claude-code'
5
6const PANE = 'snake'
7const BEST_KEY = 'best'
8
9// Held by the host, so a hot reload keeps the game's mode and scores.
10const isPlaying = atom({ plugin: 'snake', key: 'isPlaying' } as const, false)
11const score = atom({ plugin: 'snake', key: 'score' } as const, 0)
12const best = atom({ plugin: 'snake', key: 'best' } as const, 0)
13const isOn = atom({ plugin: 'snake', key: 'isOn' } as const, false)
14
15export const register: Register = on => {
16  let isWorking = false // a main-loop turn is running, whether or not Snake is on
17  on('session.start', async ($, e, next) => {
18    const r = await next(e)
19    await $.command.register({ name: 'snake', description: 'Turn Snake on or off: it plays in a pane while Claude works', argumentHint: '[on|off]' }).catch(() => {})
20    const saved = Number(await $.store.get(BEST_KEY).catch(() => 0)) || 0
21    await update($, best, b => Math.max(b, saved))
22    return r
23  })
24
25  on('turn.start', async ($, e, next) => {
26    isWorking = true
27    if (!(await read($, isOn))) return next(e) // opt-in: nothing until /snake
28    await update($, isPlaying, () => true)
29    void $.ui.open({ id: PANE, title: 'Snake' }) // seats from 144 columns when opened unasked; /snake opens it at any width
30    return next(e)
31  })
32
33  on('turn.complete', async ($, e, next) => {
34    const r = await next(e)
35    if (!e.agentId) {
36      isWorking = false
37      await update($, isPlaying, () => false)
38    }
39    return r
40  })
41
42  on('command.run', { command: 'snake' }, async ($, e) => {
43    const arg = e.args.trim().toLowerCase()
44    if (!['', 'on', 'off', 'stop'].includes(arg)) return { text: 'usage: /snake toggles it, /snake on or /snake off says which' }
45    // A bare /snake toggles; `stop` and `on` say which.
46    const isOff = arg === 'stop' || arg === 'off' || (arg !== 'on' && (await read($, isOn)))
47    if (isOff) {
48      await update($, isOn, () => false)
49      await update($, isPlaying, () => false)
50      await $.ui.close({ id: PANE })
51      return { text: 'Snake is off. /snake (or /snake on) turns it back on.' }
52    }
53    await update($, isOn, () => true)
54    await update($, isPlaying, () => isWorking) // turned on mid-turn: play now, not next turn
55    await $.ui.open({ id: PANE, title: 'Snake', focus: true })
56    return { text: 'Snake is on. It plays while Claude works. /snake again turns it off.' }
57  })
58
59  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
60    if (e.surface !== 'terminal' && e.surface !== 'desktop') {
61      const { Text } = $.ui.resolve(e)
62      return <Text dimColor>Snake plays in the terminal or desktop app.</Text>
63    }
64    const { Client } = $.ui.resolve(e)
65    const props = { isPlaying: await read($, isPlaying), best: await read($, best) }
66    // An exact size: a percentage height has nothing to stretch against inside a pane.
67    const width = Math.max(1, e.props.bodyColumns)
68    const height = Math.max(1, e.props.scroll.bodyRows - 1)
69    return <Client key="snake" module="./game.tsx" props={props} width={width} height={height} />
70  })
71
72  // The game posts its score whenever it eats or crashes.
73  on('ui.message', async ($, e) => {
74    const data = e.data as { score?: unknown }
75    if (e.element !== 'snake' || typeof data?.score !== 'number') return {}
76    const now = data.score
77    await update($, score, () => now)
78    const before = await read($, best)
79    if (now > before) {
80      await update($, best, () => now)
81      await $.store.set(BEST_KEY, now)
82    }
83    return {}
84  })
85
86  on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
87    const playing = await read($, isPlaying)
88    const points = await read($, score)
89    if (!playing || points === 0) return next(e)
90    const top = await read($, best)
91    const text = e.props.message ?? e.props.word
92    return next({ ...e, props: { ...e.props, message: `${text} · 🐍 ${points} (best ${top})` } })
93  })
94}
95
hooks/game.tsx 96 lines
1// The game itself: runs on the drawing thread with its own clock and keys.
2import type { ClientModule } from 'claude-code'
3
4import { newGame, step, turn } from './snake'
5import type { Dir, Game } from './snake'
6
7export type GameProps = { isPlaying: boolean; best: number }
8type Local = { game: Game | null; live: { props: GameProps } }
9
10const TICK_MS = 120
11const MIN_COLS = 10
12const MIN_ROWS = 6
13const COLORS = { head: '#9cff57', body: '#3fb950', food: '#ff5f5f', empty: '#3a3a3a' }
14const KEYS: Record<string, Dir> = { up: 'up', down: 'down', left: 'left', right: 'right', w: 'up', s: 'down', a: 'left', d: 'right' }
15
16const SnakeGame: ClientModule<GameProps, Local> = (props, surface) => {
17  const { Box, Text } = surface.elements
18  const { cols, rows } = gridSize(surface.columns, surface.rows)
19
20  if (surface.state === undefined) {
21    const local: Local = { game: null, live: { props } }
22    surface.every(TICK_MS, () => {
23      const now = surface.state
24      if (!now) return
25      const size = gridSize(surface.columns, surface.rows)
26      if (size.cols < MIN_COLS || size.rows < MIN_ROWS) return
27      if (!now.game || now.game.cols !== size.cols || now.game.rows !== size.rows) {
28        surface.setState({ ...now, game: newGame(size.cols, size.rows, Date.now() % 100000) }) // a new or resized pane
29        return
30      }
31      if (!now.live.props.isPlaying) return
32      const r = step(now.game)
33      if (r.ate || r.crashed) surface.post({ score: r.game.score })
34      surface.setState({ ...now, game: r.game })
35    })
36    surface.onKey(e => {
37      const now = surface.state
38      const dir = KEYS[e.key.toLowerCase()]
39      if (!now?.game || !dir || !now.live.props.isPlaying) return // paused: keys do nothing
40      surface.setState({ ...now, game: turn(now.game, dir) })
41    })
42    surface.setState(local)
43    return <Text dimColor>Loading Snake…</Text>
44  }
45
46  const local = surface.state
47  local.live.props = props // the clock and keys read the latest props
48
49  if (cols < MIN_COLS || rows < MIN_ROWS) return <Text dimColor>Make the pane bigger to play</Text>
50
51  const game = local.game
52  if (!game || game.cols !== cols || game.rows !== rows) return <Text dimColor>Setting up the board…</Text>
53
54  const isPaused = !props.isPlaying
55  const best = Math.max(props.best, game.score)
56  const cells = new Map<string, 'head' | 'body' | 'food'>()
57  cells.set(`${game.food.x},${game.food.y}`, 'food')
58  game.snake.forEach((c, i) => cells.set(`${c.x},${c.y}`, i === 0 ? 'head' : 'body'))
59
60  return (
61    <Box flexDirection="column">
62      <Text>
63        <Text bold color="green">{'🐍 Snake  '}</Text>
64        <Text>{`score ${game.score}`}</Text>
65        <Text dimColor>{` · best ${best}`}</Text>
66        {isPaused && <Text color="yellow">{"  ⏸ Claude's done, your turn"}</Text>}
67      </Text>
68      {Array.from({ length: rows }, (_, y) => (
69        <Text dimColor={isPaused}>{runs(cols, y, cells).map(r => <Text color={r.color}>{r.text}</Text>)}</Text>
70      ))}
71      <Text dimColor>{isPaused ? 'Paused until Claude works again' : 'Click to play · arrows or WASD · Esc gives the keys back'}</Text>
72    </Box>
73  )
74}
75
76export default SnakeGame
77
78// Cells across and down: each cell is two characters wide, and a header and footer row sit around the board.
79function gridSize(columns: number, rows: number) {
80  return { cols: Math.floor(columns / 2), rows: rows - 2 }
81}
82
83// One row as runs of same-colored cells, so a row is a few Text nodes, not one per cell.
84function runs(cols: number, y: number, cells: Map<string, 'head' | 'body' | 'food'>) {
85  const out: { color: string; text: string }[] = []
86  for (let x = 0; x < cols; x++) {
87    const kind = cells.get(`${x},${y}`)
88    const color = COLORS[kind ?? 'empty']
89    const text = kind ? '██' : '· '
90    const last = out[out.length - 1]
91    if (last && last.color === color) last.text += text
92    else out.push({ color, text })
93  }
94  return out
95}
96
hooks/snake.ts 65 lines
1// Snake's rules: pure functions over a plain game value, no $ and no timers.
2
3export type Dir = 'up' | 'down' | 'left' | 'right'
4export type Cell = { x: number; y: number }
5export type Game = {
6  cols: number
7  rows: number
8  snake: Cell[] // head first
9  dir: Dir
10  nextDir: Dir
11  food: Cell
12  score: number
13  seed: number
14}
15
16const START_LENGTH = 3
17const MOVES: Record<Dir, Cell> = { up: { x: 0, y: -1 }, down: { x: 0, y: 1 }, left: { x: -1, y: 0 }, right: { x: 1, y: 0 } }
18const OPPOSITE: Record<Dir, Dir> = { up: 'down', down: 'up', left: 'right', right: 'left' }
19
20export function newGame(cols: number, rows: number, seed = 1): Game {
21  const y = Math.floor(rows / 2)
22  const x = Math.floor(cols / 2)
23  const snake = Array.from({ length: START_LENGTH }, (_, i) => ({ x: x - i, y }))
24  const placed = placeFood(cols, rows, snake, seed)
25  return { cols, rows, snake, dir: 'right', nextDir: 'right', food: placed.food, score: 0, seed: placed.seed }
26}
27
28// Queues a turn for the next step; a 180° turn is ignored.
29export function turn(game: Game, dir: Dir): Game {
30  if (dir === OPPOSITE[game.dir] || dir === game.nextDir) return game
31  return { ...game, nextDir: dir }
32}
33
34export function step(game: Game): { game: Game; ate: boolean; crashed: boolean } {
35  const dir = game.nextDir
36  const head = game.snake[0]!
37  const next = { x: head.x + MOVES[dir].x, y: head.y + MOVES[dir].y }
38  const ate = next.x === game.food.x && next.y === game.food.y
39  const body = ate ? game.snake : game.snake.slice(0, -1)
40
41  const hitWall = next.x < 0 || next.y < 0 || next.x >= game.cols || next.y >= game.rows
42  const hitSelf = body.some(c => c.x === next.x && c.y === next.y)
43  if (hitWall || hitSelf) {
44    return { game: newGame(game.cols, game.rows, game.seed), ate: false, crashed: true }
45  }
46
47  const snake = [next, ...body]
48  if (!ate) return { game: { ...game, snake, dir }, ate, crashed: false }
49  const placed = placeFood(game.cols, game.rows, snake, game.seed)
50  return { game: { ...game, snake, dir, food: placed.food, seed: placed.seed, score: game.score + 1 }, ate, crashed: false }
51}
52
53// A free cell for the food, from a small seeded random generator.
54function placeFood(cols: number, rows: number, snake: Cell[], seed: number) {
55  let s = seed
56  for (let tries = 0; tries < 1000; tries++) {
57    s = (s * 1103515245 + 12345) % 2147483648
58    const x = s % cols
59    s = (s * 1103515245 + 12345) % 2147483648
60    const y = s % rows
61    if (!snake.some(c => c.x === x && c.y === y)) return { food: { x, y }, seed: s }
62  }
63  return { food: { x: 0, y: 0 }, seed: s }
64}
65
types/index.d.ts 8 lines
1export type SnakeScores = { score: number; best: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'snake': { isPlaying: boolean; score: number; best: number; isOn: boolean }
6  }
7}
8