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

<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>
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.
| Mod | What it does | Command | |
|---|---|---|---|
| 🛰️ | mission-control | Live map of agents, tool calls and the code they touch | /mission |
| 📊 | context-bar | Your context window as one stacked bar, a color per category, with token counts and where it compacts | /context-bar |
| 🌦️ | token-weather | Context fill from Clear to Compact soon, plus a prompt-cache countdown | |
| ⏱️ | cache-clock | A 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-i | Goal, doing now, waiting on you, next step | /where |
| ➡️ | next-steps | 2 or 3 likely next prompts after each turn, one key to draft one | 1 2 3, 0 hides |
| 💰 | usage-meter | 5-hour and 7-day plan usage, the reset countdown and the session's cost | |
| 💳 | openai-balance | Your 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-radar | One live line per running subagent | /radar |
| 🔍 | review-watch | One 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-lanes | Whether this session has a browser, and who holds it | /browser |
| 🕌 | prayer-times | The current prayer and how long is left, the next one, and zawal. Computed on your computer, Hanafi or standard Asr | /prayers |
| 🎬 | replay-theater | Steps through the last turn's edits, one diff at a time | /replay |
| 📝 | md-preview | Renders the Markdown files Claude edits like GitHub does, with before and after side by side. Needs Chrome and a terminal that shows images | /md |
| Mod | What it does | Command | |
|---|---|---|---|
| 💥 | blast-radius | Holds rm -r, force pushes and migrations, shows what they'd delete, cancels after 60 s with no answer |
| Mod | What it does | Command | |
|---|---|---|---|
| 🔀 | switchboard | Picks 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 |
These are built around my own tools and rules. Fork them and change the rules to yours.
| Mod | What it does | Command | |
|---|---|---|---|
| 👀 | glance | One line with what needs you: next meeting, PRs, Linear issues, Slack DMs. Needs gh and the Google Calendar, Linear and Slack connectors | /glance |
| 🚦 | merge-gate | Holds 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-guard | Enforces my writing and git rules: rewrites em dashes, asks before --amend, unformatted pushes, emails and phone numbers in notes | |
| 💾 | session-saver | Saves where you left off, shows it on resume. Needs unpause | /park [note] |
| Mod | What it does | Command | |
|---|---|---|---|
| 📱 | reels | YouTube Shorts while Claude works, pauses when it's done | /reels |
| 🐍 | snake | Snake while Claude works | /snake |
| 🎵 | now-playing | What 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>
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.

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, livec shows the code map, with import arrowsThe Code view also needs macOS, Google Chrome and a terminal that shows images (Ghostty, kitty, iTerm2). The Who view works everywhere.
hooks/register.tsx 95 lines1// 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}
95hooks/game.tsx 96 lines1// 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}
96hooks/snake.ts 65 lines1// 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}
65types/index.d.ts 8 lines1export 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