After each turn, 2 or 3 likely next prompts above the prompt. Press 1, 2 or 3 in an empty prompt to draft one, 0 to dismiss.

<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 179 lines1// Next Steps: after each turn, 2 or 3 likely next prompts above the input.
2// In an empty prompt, 1 to 3 drafts one (nothing sends on its own) and 0 dismisses.
3import { atom, read, update } from 'claude-code'
4import type { EngineInterface, Register } from 'claude-code'
5
6import type { NextSteps } from '../types'
7
8const MODEL = 'haiku'
9const MAX_ITEMS = 3
10const MAX_CHARS = 60 // what the model is asked for
11const MAX_KEPT = 120 // a longer line is dropped, never cut: a cut draft would send half a prompt
12const MIN_ANSWER = 20 // a reply shorter than this is not worth a model call
13
14// Held by the host, so the list survives a hot reload of this file.
15const steps = atom({ plugin: 'next-steps', key: 'steps' } as const, null as NextSteps | null)
16// True while the list is on screen. where-am-i reads it and leaves out its own "next" meanwhile.
17const active = atom({ plugin: 'next-steps', key: 'active' } as const, false)
18
19export const register: Register = on => {
20 // The turn a suggestion may still be shown for. A submit or a new turn moves it on.
21 let latest = ''
22 let submits = 0
23 // A survey answers bare digits in the band: leave them to it.
24 let hasSurvey = false
25
26 on('prompt.submit', async ($, e, next) => {
27 latest = `submit-${++submits}`
28 await setSteps($, () => null)
29 return next(e)
30 })
31
32 on('turn.start', async ($, e, next) => {
33 latest = e.turnId
34 await setSteps($, () => null)
35 return next(e)
36 })
37
38 on('turn.complete', async ($, e, next) => {
39 const r = await next(e)
40 if (e.agentId) return r // a subagent's turn, not the person's
41 latest = e.turnId // also for a turn that raised no turn.start, and after a hot reload
42 if (e.reason !== 'answer' || e.answer.trim().length < MIN_ANSWER) return r
43 void suggest($, e.turnId, e.answer, () => latest).catch(() => {}) // in the background, so the turn ends at once
44 return r
45 })
46
47 // A digit typed into an empty prompt picks a suggestion instead of landing as text.
48 // A paste carries no key, so a pasted "1" lands as typed.
49 on('prompt.edit', async ($, e, next) => {
50 if (hasSurvey || !e.key || e.key.ctrl || e.key.meta || e.text !== '' || e.start !== 0 || e.end !== 0 || !/^[0-9]$/.test(e.inputText)) return next(e)
51 const s = await read($, steps)
52 if (!s) return next(e)
53 if (e.inputText === '0') {
54 await setSteps($, () => null)
55 return { text: '', cursor: 0 } // consumed: the box stays empty
56 }
57 const pick = s.items[Number(e.inputText) - 1]
58 if (!pick) return next(e)
59 return next({ ...e, inputText: pick }) // a draft: the person edits it or presses Enter
60 })
61
62 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
63 const rest = await next(e) // what other mods and Claude Code draw here stays
64 hasSurvey = e.props.hasSurvey
65 const s = await read($, steps)
66 if (e.props.hasSurvey || e.props.isWorking || !s || s.items.length === 0) return rest
67
68 const { Box, Text } = $.ui.resolve(e)
69 const sep = <Text dimColor>{' · '}</Text>
70 const item = (text: string, i: number) => [
71 <Text color="cyan" bold>{`${i + 1} `}</Text>,
72 <Text>{text}</Text>,
73 ]
74 const dismiss = [<Text color="cyan" bold>{'0 '}</Text>, <Text dimColor>dismiss</Text>]
75
76 const lines =
77 // One per row needs a row each; a short band gets the one row, cut at its end.
78 fitsOnOneLine(s.items, e.props.bodyColumns) || e.props.maxRows < s.items.length + 2 ? (
79 <Text wrap="truncate-end">
80 <Text dimColor>{'next: '}</Text>
81 {s.items.flatMap((t, i) => [...item(t, i), sep])}
82 {dismiss}
83 </Text>
84 ) : (
85 <Box flexDirection="column">
86 {s.items.map((t, i) => (
87 <Text wrap="truncate-end">
88 <Text dimColor>{i === 0 ? 'next: ' : ' '}</Text>
89 {item(t, i)}
90 {i === s.items.length - 1 && sep}
91 {i === s.items.length - 1 && dismiss}
92 </Text>
93 ))}
94 </Box>
95 )
96
97 return (
98 <Box flexDirection="column">
99 <Box paddingX={1}>{lines}</Box>
100 {rest}
101 </Box>
102 )
103 })
104}
105
106// Writes the list, then whether it shows, for where-am-i.
107async function setSteps($: EngineInterface, fn: (before: NextSteps | null) => NextSteps | null) {
108 const after = await update($, steps, fn)
109 const showing = after !== null && after.items.length > 0
110 if ((await read($, active)) !== showing) await update($, active, () => showing)
111}
112
113// The whole list on one row, or one suggestion per row when it would not fit.
114export function fitsOnOneLine(items: string[], columns: number) {
115 const width = 'next: '.length + items.reduce((n, t) => n + 2 + t.length + 5, 0) + '0 dismiss'.length
116 return width <= columns - 2 // the band's padding
117}
118
119async function suggest($: EngineInterface, turnId: string, answer: string, current: () => string) {
120 // Wait for background agents: the main agent picks up again once they finish.
121 const agents = await $.agent.list().catch(() => [])
122 if (agents.some(a => a.status === 'running')) return
123
124 const all = (await $.session.messages().catch(() => [])).filter(m => m.text.trim() !== '')
125 if (all.at(-1)?.role === 'assistant') all.pop() // the latest reply goes in on its own below
126 const messages = all.slice(-6)
127 const r = await $.model.complete({
128 model: MODEL,
129 maxTokens: 200,
130 timeoutMs: 20_000,
131 system:
132 'You predict what a person will type next to their coding assistant, after reading a transcript of their session. ' +
133 `Give 2 or 3 short, concrete follow-up prompts, each an instruction in the imperative, under ${MAX_CHARS} characters, ` +
134 'specific to what just happened (name the file, test or feature). Plain words, no em dashes, no numbering, no quotes. ' +
135 'One per line and nothing else. If nothing sensible follows, reply with the single word NONE.\n' +
136 'Example reply:\nRun the login tests again\nAdd the same check to signup.ts\nOpen a draft PR',
137 prompt: [
138 `<transcript>\n${messages.map(m => `[${m.role === 'user' ? 'person' : 'assistant'}] ${m.text.slice(0, 800)}`).join('\n')}\n</transcript>`,
139 `<latest_reply>\n${answer.slice(0, 2000)}\n</latest_reply>`,
140 'Write the follow-up prompts the person is likely to send next: the lines only, not a reply to the transcript.',
141 ].join('\n\n'),
142 })
143 if (!r.isAnswered) return
144 const items = parseSteps(r.text)
145 // Checked inside the write, so a submit or a new turn that lands meanwhile always wins.
146 await setSteps($, before => (current() !== turnId ? before : items.length > 0 ? { turnId, items } : null))
147}
148
149// The model's lines, cleaned: no fences, bullets, numbers or em dashes; at most three.
150// A preamble, a "nothing to add" or a line too long to draft whole is dropped.
151export function parseSteps(text: string): string[] {
152 const body = text.replace(/```[a-z]*\n?/gi, '').trim()
153 let lines = body.split('\n')
154 if (body.startsWith('[') || body.startsWith('{')) {
155 try {
156 const o = JSON.parse(body) as unknown
157 const arr = Array.isArray(o) ? o : Object.values(o as Record<string, unknown>).find(Array.isArray)
158 lines = Array.isArray(arr) ? arr.filter((x): x is string => typeof x === 'string') : []
159 } catch {
160 // not JSON after all: read it as lines
161 }
162 }
163 const out: string[] = []
164 for (const line of lines) {
165 const clean = line
166 .replace(/\*\*/g, '')
167 .replace(/^\s*(?:[-*•]|\d+[.):])\s*/, '')
168 .replace(/^["'`]+|["'`,]+$/g, '')
169 .replace(/\s*\u2014\s*/g, ', ')
170 .replace(/\.$/, '')
171 .trim()
172 if (clean === '' || clean.endsWith(':') || clean.length > MAX_KEPT) continue
173 if (/^(none|no |nothing|here are|here's|sure)/i.test(clean) || /^[[\]{}]$/.test(clean)) continue
174 if (!out.some(o => o.toLowerCase() === clean.toLowerCase())) out.push(clean)
175 if (out.length === MAX_ITEMS) break
176 }
177 return out
178}
179types/index.d.ts 8 lines1export type NextSteps = { turnId: string; items: string[] }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'next-steps': { steps: NextSteps | null; active: boolean }
6 }
7}
8