A live line above the prompt for each running code review: Codex or a review subagent, its model, what it reviews, time, and Codex's latest output. A toast…

<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 290 lines1// Review Watch: is my code review still running?
2// The band above the prompt has one live line per running review: a `codex review`
3// shell command or a subagent whose description says "review". It shows the model, what
4// is under review, the time, and Codex's latest output line. When a review ends a toast
5// says so, with the findings Codex reported, and its line shows a check for 30 s.
6import { atom, read, update } from 'claude-code'
7import type { EngineInterface, Register, Timer } from 'claude-code'
8
9import type { Review } from '../types'
10
11const SHOW_DONE_MS = 30_000 // a finished review stays in the band this long
12const BAND_ROWS = 4
13const POLL_MS = 3_000
14const UNSEEN_POLLS = 3 // a review whose process never shows up in ps is over after this many polls
15
16// Held by the host, so the reviews survive a hot reload of this file.
17const reviews = atom({ plugin: 'review-watch', key: 'reviews' } as const, [] as Review[])
18const now = atom({ plugin: 'review-watch', key: 'now' } as const, 0)
19
20let isPolling = false // a slow ps must not let two polls overlap
21
22export const register: Register = on => {
23 let cwd = ''
24 let timers: Timer[] = []
25
26 on('session.start', async ($, e, next) => {
27 const r = await next(e)
28 cwd = e.cwd
29 // A review agent that is no longer running (a resumed or new session) has no line to keep.
30 const live = new Set((await $.agent.list().catch(() => [])).filter(a => a.status === 'running').map(a => a.id))
31 await update($, reviews, list => list.filter(v => v.kind === 'codex' || v.status !== 'running' || live.has(v.id)))
32 for (const t of timers) t.cancel()
33 timers = []
34 // A clock for the elapsed times, ticking only while something shows.
35 timers.push($.clock.every(1000, () => {
36 void (async () => {
37 if ((await read($, reviews)).some(v => isShown(v, Date.now() - 3_000))) await update($, now, () => Date.now())
38 })().catch(() => {})
39 }))
40 timers.push($.clock.every(POLL_MS, () => void poll($).catch(() => {})))
41 return r
42 })
43
44 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
45 if (!isCodexReview(e.command)) return next(e)
46 const id = `codex-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`
47 const one: Review = {
48 id,
49 kind: 'codex',
50 model: modelOf(e.command) ?? (await configModel($)),
51 label: labelOf(e.command),
52 status: 'running',
53 startedAt: Date.now(),
54 key: keyOf(e.command),
55 out: outputOf(e.command, cwd, (await $.env.get('HOME').catch(() => undefined)) ?? ''),
56 polls: 0,
57 last: 'starting',
58 }
59 await update($, reviews, list => [one, ...list].slice(0, 20))
60 const r = await next(e)
61 if (r.deny || r.isError || !e.run_in_background) {
62 await finish($, id, Boolean(r.deny || r.isError)) // it ran in the foreground: it is over
63 } else if (!one.out) {
64 const path = /written to: (\S+\.output)/.exec(r.text ?? '')?.[1] // where Claude Code keeps a background shell's output
65 if (path) await update($, reviews, list => list.map(v => (v.id === id ? { ...v, out: path } : v)))
66 }
67 return r
68 })
69
70 on('agent.spawn', async ($, e, next) => {
71 const r = await next(e)
72 if (r.agentId && isReviewAgent(e.description ?? '')) {
73 const one: Review = { id: r.agentId, kind: 'agent', model: e.model || r.model || 'inherit', label: e.description, status: 'running', startedAt: Date.now(), last: e.subagentType }
74 await update($, reviews, list => [one, ...list.filter(v => v.id !== one.id)].slice(0, 20))
75 }
76 return r
77 })
78
79 on('turn.complete', async ($, e, next) => {
80 const r = await next(e)
81 if (e.agentId) await finish($, e.agentId, e.reason === 'error' || e.reason === 'aborted')
82 return r
83 })
84
85 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
86 const rest = await next(e) // what other mods and Claude Code draw here stays
87 await read($, now) // subscribes the band to the clock
88 const shown = (await read($, reviews)).filter(v => isShown(v))
89 if (e.props.hasSurvey || shown.length === 0) return rest
90 const { Box, Text } = $.ui.resolve(e)
91 return (
92 <Box flexDirection="column">
93 {shown.slice(0, BAND_ROWS).map(v => (
94 <Box flexDirection="column">
95 <Text wrap="truncate-end">
96 <Text color={color(v)} bold>{` ${icon(v)} ${v.kind === 'codex' ? 'codex' : 'agent'}`}</Text>
97 <Text>{` · ${v.model} · ${v.label}`}</Text>
98 {v.status !== 'running' && v.findings && <Text bold>{` · ${findingsText(v.findings)}`}</Text>}
99 <Text dimColor>{` ${elapsed(v)}`}</Text>
100 </Text>
101 {v.status === 'running' && v.kind === 'codex' && v.last !== '' && (
102 <Text dimColor wrap="truncate-end">{` └ ${v.last}`}</Text>
103 )}
104 </Box>
105 ))}
106 {shown.length > BAND_ROWS && <Text dimColor>{` +${shown.length - BAND_ROWS} more`}</Text>}
107 {rest}
108 </Box>
109 )
110 })
111}
112
113// Checks each running Codex review: is its process still there, and what did it print last.
114async function poll($: EngineInterface) {
115 if (isPolling) return
116 isPolling = true
117 try {
118 await pollOnce($)
119 } finally {
120 isPolling = false
121 }
122}
123
124async function pollOnce($: EngineInterface) {
125 const running = (await read($, reviews)).filter(v => v.kind === 'codex' && v.status === 'running')
126 if (running.length === 0) return
127 const ps = await $.process.run(['ps', '-axww', '-o', 'command='], { timeoutMs: 5_000 })
128 if (ps.exitCode !== 0) return
129 const lines = ps.stdout.split('\n').filter(l => isCodexReview(l)).map(plain)
130 for (const v of running) {
131 const isAlive = lines.some(l => `${l} `.includes(`${v.key ?? 'codex review'} `)) // whole arguments: `--base main` is not `--base main2`
132 const polls = (v.polls ?? 0) + 1
133 if (!isAlive && (v.seen || polls >= UNSEEN_POLLS)) {
134 await finish($, v.id, false)
135 continue
136 }
137 const last = v.out ? await lastLine($, v.out) : v.last
138 await update($, reviews, list => list.map(x => (x.id === v.id ? { ...x, polls, seen: x.seen || isAlive, last } : x)))
139 }
140}
141
142async function finish($: EngineInterface, id: string, failed: boolean) {
143 // Claimed in one update, so two callers (a poll and the tool call) never both toast.
144 let v: Review | undefined
145 await update($, reviews, list =>
146 list.map(x => (x.id === id && x.status === 'running' ? (v = { ...x, status: failed ? 'failed' : 'done', endedAt: Date.now() }) : x)),
147 )
148 if (!v) return
149 const done = v
150 const findings = done.kind === 'codex' && done.out && !failed ? findingsOf(await tail($, ['-c', '40000', '--', done.out])) : undefined
151 if (findings) await update($, reviews, list => list.map(x => (x.id === id ? { ...x, findings } : x)))
152 const what = done.kind === 'codex' ? `codex review (${done.model})` : `${done.label} (${done.model})`
153 const extra = findings ? ` · ${findingsText(findings)}` : ''
154 $.ui.toast(`${failed ? '✗' : '✓'} ${what} ${failed ? 'stopped' : 'done'} · ${elapsed(done)}${extra}`)
155}
156
157async function tail($: EngineInterface, args: string[]) {
158 const r = await $.process.run(['tail', ...args], { timeoutMs: 5_000 }).catch(() => null)
159 return r && r.exitCode === 0 ? r.stdout : ''
160}
161
162async function lastLine($: EngineInterface, file: string) {
163 const lines = clean(await tail($, ['-n', '30', '--', file])).split('\n').map(l => l.trim()).filter(Boolean)
164 return lines.at(-1)?.slice(0, 200) ?? ''
165}
166
167// The model from ~/.codex/config.toml, for a review run without one on its command line.
168async function configModel($: EngineInterface) {
169 const r = await $.process
170 .run(['sh', '-c', 'grep -E "^model *=" "$HOME/.codex/config.toml" | head -n 1'], { timeoutMs: 3_000 })
171 .catch(() => null)
172 return (r && /=\s*["']?([\w.:-]+)/.exec(r.stdout)?.[1]) || 'default'
173}
174
175// `codex review` where a command starts (after `;`, `&&`, `|`, `timeout 900` and the like), flags before `review` allowed.
176const CODEX_REVIEW =
177 /(?:^|[;&|(])\s*(?:(?:timeout|nohup|nice|env|time|command)(?:\s+[-\w.=:]+)*\s+)?(?:\S*\/)?codex(?:\s+-{1,2}[\w-]+(?:[\s=]+(?!-)\S+)?)*\s+review(?=\s|$)/
178
179export function isCodexReview(command: string) {
180 return CODEX_REVIEW.test(command)
181}
182
183// A subagent whose description says review (not "preview" or "interview").
184export function isReviewAgent(description: string) {
185 return /\breview(?:s|er|ers|ing)?\b/i.test(description)
186}
187
188const TITLE = /--title(?:\s+|=)(?:(["'])(.*?)\1|([^\s"';&|<>]+))/
189
190// The `codex ... review ...` part of a command, up to where the next command starts.
191function codexPart(command: string) {
192 const m = CODEX_REVIEW.exec(command)
193 if (!m) return ''
194 const rest = command.slice(m.index).replace(TITLE, '--title _')
195 return rest.split(/\s*(?:&&|\|\||;|\|(?!&))\s*/)[0]!
196}
197
198export function modelOf(command: string) {
199 const part = codexPart(command)
200 return /\s-c\s+["']?model\s*=\s*\\?["']?([\w.:-]+)/.exec(part)?.[1] ?? /\s(?:--model|-m)(?:\s+|=)["']?([\w.:-]+)/.exec(part)?.[1]
201}
202
203export function labelOf(command: string) {
204 const title = titleOf(command)
205 if (title) return title
206 const part = codexPart(command)
207 const commit = /--commit(?:\s+|=)["']?(\w+)/.exec(part)?.[1]
208 if (commit) return `commit ${commit.slice(0, 7)}`
209 const base = /--base(?:\s+|=)["']?([^\s"']+)/.exec(part)?.[1]
210 return base ? `changes vs ${base}` : 'uncommitted changes'
211}
212
213function titleOf(command: string) {
214 const m = TITLE.exec(command.slice(Math.max(0, command.search(CODEX_REVIEW))))
215 return m ? (m[2] ?? m[3] ?? '') : ''
216}
217
218// What finds this review's process in ps: its arguments as ps prints them (quotes gone).
219export function keyOf(command: string) {
220 const m = CODEX_REVIEW.exec(command)
221 if (!m) return 'codex review'
222 const rest = command.slice(m.index + m[0].search(/codex\s/))
223 // Up to the first redirect or the next command, past a quoted title.
224 const masked = rest.replace(TITLE, x => x.replace(/[;&|<>]/g, '_'))
225 const cut = masked.search(/\s(?:\d?>|&>)|\s*(?:&&|\|\||;|\|)/)
226 return plain(cut >= 0 ? rest.slice(0, cut) : rest)
227}
228
229// The file the review's stdout goes to (`> file`): `~` from HOME, a relative one from a `cd` before it or the session's folder.
230export function outputOf(command: string, cwd: string, home = '') {
231 const at = Math.max(0, command.search(CODEX_REVIEW))
232 const re = /(\d|&)?>>?\s*(["']?)([^\s"'&|;<>]+)\2/g
233 const rest = command.slice(at).replace(TITLE, '--title _')
234 for (const m of rest.matchAll(re)) {
235 if (m[1] === '2' || m[3]!.startsWith('&')) continue
236 let path = m[3]!
237 if (path === '/dev/null' || path.includes('$')) return undefined
238 if (path.startsWith('~/')) {
239 if (!home) return undefined
240 path = home + path.slice(1)
241 }
242 if (path.startsWith('/')) return path
243 const cds = [...command.slice(0, at).matchAll(/(?:^|[;&|])\s*cd\s+(["']?)([^\s"';&|]+)\1/g)]
244 const dir = cds.at(-1)?.[2]
245 const base = dir ? (dir.startsWith('/') ? dir : dir.startsWith('~/') && home ? home + dir.slice(1) : `${cwd}/${dir}`) : cwd
246 return base ? `${base}/${path}` : undefined
247 }
248 return undefined
249}
250
251// Codex ends with a "Full review comments:" block of `- [P2] title — file:line` items.
252// Undefined when the output says neither (an error, a killed run, an unreadable file).
253export function findingsOf(text: string): string[] | undefined {
254 const at = text.lastIndexOf('Full review comments:')
255 if (at >= 0) return [...text.slice(at).matchAll(/^\s*-\s*\[(P\d)\]/gm)].map(m => m[1]!)
256 if (/\b(?:no (?:issues|findings|problems|bugs)|did not (?:find|identify)|didn't (?:find|identify)|looks good)\b/i.test(text.slice(-3000))) return []
257 return undefined
258}
259
260export function findingsText(findings: string[]) {
261 if (findings.length === 0) return 'no findings'
262 return `${findings.length} finding${findings.length === 1 ? '' : 's'} (${findings.join(', ')})`
263}
264
265export function isShown(v: Pick<Review, 'status' | 'endedAt'>, at = Date.now()) {
266 return v.status === 'running' || at - (v.endedAt ?? 0) < SHOW_DONE_MS
267}
268
269export function elapsed(v: Pick<Review, 'startedAt' | 'endedAt'>) {
270 const s = Math.max(0, Math.round(((v.endedAt ?? Date.now()) - v.startedAt) / 1000))
271 return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`
272}
273
274// A command line as ps prints it: no quotes, single spaces.
275function plain(text: string) {
276 return text.replace(/["']/g, '').replace(/\s+/g, ' ').trim()
277}
278
279function clean(text: string) {
280 return text.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '')
281}
282
283function icon(v: Review) {
284 return v.status === 'running' ? '⏳' : v.status === 'done' ? '✅' : '✗'
285}
286
287function color(v: Review) {
288 return v.status === 'running' ? 'cyan' : v.status === 'done' ? 'green' : 'red'
289}
290types/index.d.ts 22 lines1export type Review = {
2 id: string
3 kind: 'codex' | 'agent'
4 model: string
5 label: string
6 status: 'running' | 'done' | 'failed'
7 startedAt: number
8 endedAt?: number
9 key?: string // a Codex review: a string of its command line, to find its process
10 out?: string // a Codex review: the file its output goes to
11 seen?: boolean // a Codex review: its process has shown up in ps
12 polls?: number
13 last: string
14 findings?: string[] // a Codex review: P1, P2, ... per finding
15}
16
17declare module 'claude-code' {
18 interface PluginState {
19 'review-watch': { reviews: Review[]; now: number }
20 }
21}
22