SLOPSHOPPER

snake

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

newpanespinnercommand
★ 167v0.2.3MITupdated 2026-10-05hamzafer/claude-code-mods/mods/snake
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

<h1 align="center">Claude Code mods</h1>

<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT"></a> <a href="https://github.com/hamzafer/claude-code-mods/actions/workflows/ci.yml"><img src="https://github.com/hamzafer/claude-code-mods/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>

<a href="#-install">Install</a> · <a href="#-the-mods">All mods</a> · <a href="docs/mods.md">Docs</a> · <a href="https://claude.dev/blog/getting-started-with-claude-code-mods/">What are mods?</a>

<table> <tr> <td align="center" width="33%"><a href="docs/mods.md#-context-bar"><img src="images/context-bar.png" alt="context-bar: Claude Code context window usage as a stacked bar, a color per category" width="260"></a><br>📊 <b>context-bar</b><br>what fills your context</td> <td align="center" width="33%"><a href="docs/mods.md#-review-watch"><img src="images/review-watch.png" alt="review-watch: live lines for running Codex and subagent code reviews in Claude Code" width="260"></a><br>🔍 <b>review-watch</b><br>running code reviews, live</td> <td align="center" width="33%"><a href="docs/mods.md#-md-preview"><img src="images/md-preview.png" alt="md-preview: Markdown that Claude Code edits, rendered like GitHub next to the diff" width="260"></a><br>📝 <b>md-preview</b><br>Markdown rendered like GitHub</td> </tr> <tr> <td align="center" width="33%"><a href="docs/mods.md#-blast-radius"><img src="images/gallery/blast-radius.png" alt="blast-radius: a Claude Code hook holds rm -rf and lists the files it would delete" width="260"></a><br>💥 <b>blast-radius</b><br>see what <code>rm -rf</code> would delete</td> <td align="center" width="33%"><a href="docs/mods.md#-now-playing"><img src="images/now-playing.png" alt="now-playing: Spotify track, progress bar and synced lyrics inside Claude Code" width="260"></a><br>🎵 <b>now-playing</b><br>Spotify and its lyrics, live</td> <td align="center" width="33%"><a href="docs/mods.md#-reels-and-snake"><img src="images/reels-demo.gif" alt="reels: YouTube Shorts in a Claude Code pane while it works" width="260"></a><br>📱 <b>reels</b><br>Shorts while Claude works</td> </tr> <tr> <td align="center" width="33%"><a href="docs/mods.md#-where-am-i"><img src="images/gallery/where-am-i.png" alt="where-am-i: the session goal, current step and what waits on you, above the Claude Code prompt" width="260"></a><br>📍 <b>where-am-i</b><br>goal, now, waiting on you</td> <td align="center" width="33%"><a href="docs/mods.md#-lines-above-the-prompt"><img src="images/gallery/lines.png" alt="token-weather, usage-meter and other Claude Code status lines stacked above the prompt" width="260"></a><br>🌦️ <b>token-weather and friends</b><br>lines above the prompt</td> <td align="center" width="33%"><a href="#mission-control"><img src="images/gallery/mission-control.png" alt="mission-control: Claude Code subagents, tool calls and the files they touch, live" width="260"></a><br>🛰️ <b>mission-control</b><br>agents and the code they touch</td> </tr> </table>

🚀 Install

Add the marketplace once, then install any mod by name:

claude plugin marketplace add hamzafer/claude-code-mods
claude plugin install context-bar@claude-code-mods

Or install the general-purpose set in one go:

for m in context-bar token-weather usage-meter where-am-i next-steps agent-radar review-watch replay-theater md-preview blast-radius mission-control; do
  claude plugin install "$m@claude-code-mods"
done

Restart Claude Code after installing. To try one without installing:

git clone https://github.com/hamzafer/claude-code-mods && cd claude-code-mods
claude --plugin-dir mods/context-bar

Needs Claude Code 2.1.287+. A few mods need more (Chrome, gh, a connector); the tables say which.

🧩 The mods

👀 See what's happening

ModWhat it doesCommand
🛰️mission-controlLive map of agents, tool calls and the code they touch/mission
📊context-barYour context window as one stacked bar, a color per category, with token counts and where it compacts/context-bar
🌦️token-weatherContext fill from Clear to Compact soon, plus a prompt-cache countdown
⏱️cache-clockA prompt-cache line under your status line, from Claude Code's own figures: time left, hit rate and misses, and the tokens your next message re-caches once it goes cold. Needs Node and Claude Code 2.1.251+/cache-clock setup
📍where-am-iGoal, doing now, waiting on you, next step/where
➡️next-steps2 or 3 likely next prompts after each turn, one key to draft one1 2 3, 0 hides
💰usage-meter5-hour and 7-day plan usage, the reset countdown and the session's cost
💳openai-balanceYour OpenAI API credit: an estimated balance with a gauge, today's spend, where the money mostly went, and the last call. Needs an OpenAI organization Admin key/openai-balance
📡agent-radarOne live line per running subagent/radar
🔍review-watchOne live line per running code review (Codex or a review subagent) with the model, target, elapsed time and Codex's latest output. A toast lists the findings when it ends
🌐browser-lanesWhether this session has a browser, and who holds it/browser
🕌prayer-timesThe current prayer and how long is left, the next one, and zawal. Computed on your computer, Hanafi or standard Asr/prayers
🎬replay-theaterSteps through the last turn's edits, one diff at a time/replay
📝md-previewRenders the Markdown files Claude edits like GitHub does, with before and after side by side. Needs Chrome and a terminal that shows images/md

🛡️ Guard your repo

ModWhat it doesCommand
💥blast-radiusHolds rm -r, force pushes and migrations, shows what they'd delete, cancels after 60 s with no answer

💸 Spend less

ModWhat it doesCommand
🔀switchboardPicks the model for each subagent that doesn't name one, with OpenAI's Decisions API or Jev, from its short label only. Shows what every subagent cost/route

🔧 My setup (fork and adapt)

These are built around my own tools and rules. Fork them and change the rules to yours.

ModWhat it doesCommand
👀glanceOne line with what needs you: next meeting, PRs, Linear issues, Slack DMs. Needs gh and the Google Calendar, Linear and Slack connectors/glance
🚦merge-gateHolds gh pr merge until CI is green and Codex reviewed once. Needs gh and the Codex CLI. Reviews run on one fixed model; change it to yours/gate
📏rulebook-guardEnforces my writing and git rules: rewrites em dashes, asks before --amend, unformatted pushes, emails and phone numbers in notes
💾session-saverSaves where you left off, shows it on resume. Needs unpause/park [note]

🎮 For fun (opt-in)

ModWhat it doesCommand
📱reelsYouTube Shorts while Claude works, pauses when it's done/reels
🐍snakeSnake while Claude works/snake
🎵now-playingWhat Spotify is playing, with a progress bar, the lyric being sung, and ⏮ ⏸ ⏭ buttons. Needs macOS and the Spotify app/music

<a id="mission-control"></a>

🛰️ Flagship: mission-control

Every subagent, every tool call and every file they touch, in a pane next to the chat. Shown at 4x: two subagents building a logout feature across four files.

mission-control at 4x: two subagents and a logout feature landing across four files

Install it like any mod, restart, and type /mission (or /mission code to open the code map). q closes it.

  • 🤖 w shows the agents and every tool call, live
  • 🗺️ c shows the code map, with import arrows
  • 🔵 Blue while the agent reads a file
  • 🟠 Orange while it writes
  • 🟢 Green when done, with one line on what changed

The Code view also needs macOS, Google Chrome and a terminal that shows images (Ghostty, kitty, iTerm2). The Who view works everywhere.

📚 More

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