Step through the last turn's file edits, one diff at a time.

<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 182 lines1// Replay Theater: step through the last turn's file edits, one diff at a time.
2import { atom, read, update } from 'claude-code'
3import type { EngineInterface, Register } from 'claude-code'
4
5import type { ReplayStep } from '../types'
6
7const PANE = 'replay-theater'
8const MAX_DIFF_LINES = 400
9
10// Held by the host, so a replay survives a hot reload of this file.
11const replay = atom({ plugin: 'replay-theater', key: 'replay' } as const, [] as ReplayStep[])
12const pos = atom({ plugin: 'replay-theater', key: 'pos' } as const, 0)
13
14export const register: Register = on => {
15 let pending: ReplayStep[] = []
16 let cwd = ''
17 let toasted = false // one toast per session teaches /replay; later turns use the status line
18 let showing = false // the status line holds a count from the last turn
19
20 on('session.start', async ($, e, next) => {
21 const r = await next(e)
22 cwd = e.cwd
23 toasted = false
24 if (showing) {
25 showing = false
26 $.ui.status(undefined) // a new session starts without the last one's count
27 }
28 await $.command.register({ name: 'replay', description: "Step through the last turn's file edits" }).catch(() => {}) // a name Claude Code already has is refused: start anyway
29 return r
30 })
31
32 on('turn.start', ($, e, next) => {
33 pending = [] // a model turn begins: start a fresh replay
34 if (showing) {
35 showing = false
36 $.ui.status(undefined) // the count belongs to the last turn
37 }
38 return next(e)
39 })
40
41 // Observe only: the edit runs untouched.
42 on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
43 const r = await next(e)
44 if (!r.deny && !r.isError) {
45 pending.push({ file: e.file_path, kind: 'edit', before: e.old_string, after: e.new_string })
46 }
47 return r
48 })
49
50 on('tool.call', { tool: 'Write' }, async ($, e, next) => {
51 const before = await $.fs.read(e.file_path).catch(() => '') // read just before the write lands
52 const r = await next(e)
53 if (!r.deny && !r.isError) {
54 pending.push({ file: e.file_path, kind: 'write', before, after: e.content })
55 }
56 return r
57 })
58
59 on('turn.complete', async ($, e, next) => {
60 const r = await next(e)
61 if (!e.agentId && pending.length > 0) {
62 const steps = pending
63 pending = []
64 await update($, replay, () => steps) // one replay per turn
65 await update($, pos, () => 0)
66 const count = `${steps.length} edit${steps.length === 1 ? '' : 's'}`
67 if (!toasted) {
68 toasted = true
69 $.ui.toast(`replay-theater: ${count} last turn. Run /replay`)
70 } else {
71 showing = true
72 $.ui.status(`▶ /replay: ${count}`)
73 }
74 }
75 return r
76 })
77
78 on('command.run', { command: 'replay' }, async $ => ({
79 text: (await openReplay($)) ? 'Replaying the last turn' : 'No edits to replay yet',
80 }))
81
82 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
83 const { Box, Text, Button } = $.ui.resolve(e)
84 const steps = await read($, replay)
85 const at = Math.min(await read($, pos), Math.max(0, steps.length - 1))
86 const step = steps[at]
87
88 if (!step) {
89 return (
90 <Box flexDirection="column">
91 <Text dimColor>No edits to replay yet.</Text>
92 <Button key="close" label="Close" hotkey="q" role="dismiss" onPress={() => $.ui.close({ id: PANE })} />
93 </Box>
94 )
95 }
96
97 const width = Math.max(20, e.props.bodyColumns - 2)
98 return (
99 <Box flexDirection="column">
100 <Text>
101 <Text bold>{`Step ${at + 1}/${steps.length} `}</Text>
102 <Text dimColor>{`${step.kind === 'write' ? 'write' : 'edit'} `}</Text>
103 <Text color="cyan">{relative(step.file, cwd)}</Text>
104 </Text>
105 <Text>
106 {steps.map((_, i) => (
107 <Text color={i === at ? 'cyan' : undefined} bold={i === at} dimColor={i !== at}>
108 {i === at ? `[${i + 1}]` : ` ${i + 1} `}
109 </Text>
110 ))}
111 </Text>
112 <Box flexDirection="row" gap={1}>
113 <Button key="prev" label="Prev" hotkey="p" onPress={() => update($, pos, p => Math.max(0, p - 1))} />
114 <Button key="next" label="Next" hotkey="n" variant="primary" onPress={() => update($, pos, p => Math.min(steps.length - 1, p + 1))} />
115 <Button key="close" label="Close" hotkey="q" role="dismiss" onPress={() => $.ui.close({ id: PANE })} />
116 </Box>
117 <Text dimColor>{'─'.repeat(width)}</Text>
118 {diffLines(step).map(line => (
119 <Text color={line.sign === '+' ? 'green' : line.sign === '-' ? 'red' : undefined} dimColor={line.sign === ' '} wrap="truncate-end">
120 {`${line.sign} ${line.text}`}
121 </Text>
122 ))}
123 </Box>
124 )
125 })
126}
127
128async function openReplay($: EngineInterface) {
129 if ((await read($, replay)).length === 0) return false
130 await update($, pos, () => 0)
131 await $.ui.open({ id: PANE, title: 'Replay Theater', focus: true })
132 return true
133}
134
135type DiffLine = { sign: '+' | '-' | ' '; text: string }
136
137// A line diff (LCS) of the step's before and after text, capped for large files.
138export function diffLines(step: ReplayStep): DiffLine[] {
139 const a = lines(step.before)
140 const b = lines(step.after)
141 if (a.length + b.length > MAX_DIFF_LINES * 2) {
142 return [{ sign: ' ', text: `(large change: ${a.length} → ${b.length} lines, diff not drawn)` }]
143 }
144 const n = a.length
145 const m = b.length
146 const lcs: number[][] = Array.from({ length: n + 1 }, () => new Array<number>(m + 1).fill(0))
147 for (let i = n - 1; i >= 0; i--) {
148 for (let j = m - 1; j >= 0; j--) {
149 lcs[i]![j] = a[i] === b[j] ? lcs[i + 1]![j + 1]! + 1 : Math.max(lcs[i + 1]![j]!, lcs[i]![j + 1]!)
150 }
151 }
152 const out: DiffLine[] = []
153 let i = 0
154 let j = 0
155 while (i < n && j < m) {
156 if (a[i] === b[j]) {
157 out.push({ sign: ' ', text: a[i]! })
158 i++
159 j++
160 } else if (lcs[i + 1]![j]! >= lcs[i]![j + 1]!) {
161 out.push({ sign: '-', text: a[i++]! })
162 } else {
163 out.push({ sign: '+', text: b[j++]! })
164 }
165 }
166 while (i < n) out.push({ sign: '-', text: a[i++]! })
167 while (j < m) out.push({ sign: '+', text: b[j++]! })
168 return out
169}
170
171// A file's lines, without the empty one after a trailing newline.
172function lines(text: string) {
173 return text === '' ? [] : text.replace(/\n$/, '').split('\n')
174}
175
176// The path from the working directory, or its last two parts when it is outside.
177function relative(file: string, cwd: string) {
178 if (cwd && file.startsWith(`${cwd}/`)) return file.slice(cwd.length + 1)
179 const parts = file.split('/')
180 return parts.length > 3 ? `…/${parts.slice(-2).join('/')}` : file
181}
182types/index.d.ts 8 lines1export type ReplayStep = { file: string; kind: 'edit' | 'write'; before: string; after: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'replay-theater': { replay: ReplayStep[]; pos: number }
6 }
7}
8