A prompt-cache line under your status line: minutes left while warm, and how many tokens the next message re-caches once cold. /cache-clock setup adds it.

<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.ts 231 lines1// Cache Clock: a prompt-cache line under the status line, drawn from the
2// status line input's `prompt_cache`. Plugins cannot register a status line,
3// so /cache-clock setup points statusLine at a script that runs the person's
4// own status line first and adds the cache line under it.
5import type { EngineInterface, Register } from 'claude-code'
6
7// The scripts setup copies out of the plugin. They live in the config dir, not
8// in the plugin's folder, which changes with every version of the mod.
9const SCRIPTS = ['cache-clock.mjs', 'render.mjs']
10// What marks a statusLine command as this mod's.
11const MARK = 'cache-clock/cache-clock.mjs'
12const REFRESH_SECONDS = 30
13// The first Claude Code that sends `prompt_cache` to the status line.
14const MIN_VERSION = [2, 1, 251]
15
16type StatusLine = { type?: string; command?: string; refreshInterval?: number; [key: string]: unknown }
17// What setup leaves for remove and for the script: the settings file it wrote,
18// the statusLine that file had before, and the command the script runs first.
19// One per settings file (setup-<key>.json), so setups in several projects and
20// in the user settings each keep their own original.
21type Saved = { file: string; previous: StatusLine | null; wrapped: string | null }
22
23export const register: Register = on => {
24 on('session.start', async ($, e, next) => {
25 const r = await next(e)
26 await $.command.register({
27 name: 'cache-clock',
28 description: 'Add a prompt-cache line under the status line; /cache-clock remove takes it out',
29 argumentHint: '[setup|remove]',
30 }).catch(() => {}) // a name Claude Code already has is refused: start anyway
31 void refreshScripts($).catch(() => {})
32 return r
33 })
34
35 on('command.run', { command: 'cache-clock' }, async ($, e) => {
36 const word = e.args.trim().toLowerCase()
37 try {
38 if (word === 'setup') return { text: await setup($) }
39 if (word === 'remove') return { text: await remove($) }
40 if (word === '') return { text: await status($) }
41 return { text: 'Usage: /cache-clock [setup|remove]' }
42 } catch (err) {
43 return { text: `cache-clock: ${err instanceof Error ? err.message : String(err)}` }
44 }
45 })
46}
47
48async function setup($: EngineInterface) {
49 const dir = await clockDir($)
50 const found = await inForce($)
51 if (found.source === 'policy' || found.source === 'flag') {
52 const where = found.source === 'policy' ? 'managed settings' : '--settings'
53 return `Your status line comes from ${where}, which cache-clock can't change. Nothing changed.`
54 }
55 if (isOurs(found.statusLine)) {
56 await copyScripts($, dir)
57 return 'cache-clock is already set up. Its scripts are up to date.'
58 }
59
60 const file = await targetFile($, found.source)
61 const settings = await readJson($, file)
62 const current = found.statusLine
63 const wrapped = typeof current?.command === 'string' && current.command.trim() ? current.command : null
64 const saved: Saved = { file, previous: asStatusLine(settings.statusLine), wrapped }
65 const key = keyFor(file)
66
67 await copyScripts($, dir)
68 await $.fs.write(`${dir}/setup-${key}.json`, JSON.stringify(saved, null, 2) + '\n')
69
70 const { command: _command, refreshInterval, ...kept } = current ?? {}
71 settings.statusLine = {
72 ...kept,
73 type: 'command',
74 command: `node "${shellQuoted(`${dir}/cache-clock.mjs`)}" ${key}`,
75 refreshInterval: typeof refreshInterval === 'number' && refreshInterval < REFRESH_SECONDS ? refreshInterval : REFRESH_SECONDS,
76 }
77 await $.fs.write(file, JSON.stringify(settings, null, 2) + '\n')
78
79 const lines = [
80 wrapped
81 ? `cache-clock set up in ${file}. Your status line stays as it is, with the cache line under it.`
82 : `cache-clock set up in ${file}. The cache line is your status line.`,
83 'It shows up after Claude\'s next reply. /cache-clock remove puts things back.',
84 ...(await warnings($)),
85 ]
86 return lines.join('\n')
87}
88
89// Takes out the setup in force here: the user settings', or this project's local one.
90async function remove($: EngineInterface) {
91 const dir = await clockDir($)
92 const found = await inForce($)
93 const file = await targetFile($, isOurs(found.statusLine) ? found.source : 'user')
94 const key = keyFor(file)
95 const saved = await readSaved($, `${dir}/setup-${key}.json`)
96 if (!saved) return 'cache-clock is not set up here. Nothing changed.'
97
98 const settings = await readJson($, saved.file)
99 if (!isOurs(asStatusLine(settings.statusLine))) {
100 await $.fs.write(`${dir}/setup-${key}.json`, '{}\n')
101 return `The status line in ${saved.file} was changed since setup, so it was left as it is.`
102 }
103 if (saved.previous) settings.statusLine = saved.previous
104 else delete settings.statusLine
105 await $.fs.write(saved.file, JSON.stringify(settings, null, 2) + '\n')
106 await $.fs.write(`${dir}/setup-${key}.json`, '{}\n')
107 return saved.previous
108 ? `cache-clock removed. Your status line in ${saved.file} is back as it was.`
109 : `cache-clock removed from ${saved.file}.`
110}
111
112async function status($: EngineInterface) {
113 const found = await inForce($)
114 if (isOurs(found.statusLine)) {
115 return ['cache-clock is on. /cache-clock remove takes it out.', ...(await warnings($))].join('\n')
116 }
117 return 'cache-clock is off. /cache-clock setup adds the cache line under your status line.'
118}
119
120// After an update of the mod, the copies setup made catch up with the new scripts.
121async function refreshScripts($: EngineInterface) {
122 if (!isOurs((await inForce($)).statusLine)) return
123 const dir = await clockDir($)
124 for (const name of SCRIPTS) {
125 const fresh = await $.fs.read(`${$.plugin.root}/statusline/${name}`)
126 const copy = await $.fs.read(`${dir}/${name}`).catch(() => null)
127 if (copy !== fresh) await $.fs.write(`${dir}/${name}`, fresh)
128 }
129}
130
131async function copyScripts($: EngineInterface, dir: string) {
132 for (const name of SCRIPTS) {
133 await $.fs.write(`${dir}/${name}`, await $.fs.read(`${$.plugin.root}/statusline/${name}`))
134 }
135}
136
137async function warnings($: EngineInterface) {
138 const out: string[] = []
139 const node = await $.process.run(['node', '--version'], { timeoutMs: 5000 }).catch(() => null)
140 if (!node || node.exitCode !== 0) out.push('Warning: node is not on your PATH. The cache line needs Node to run.')
141 const { base, version } = await $.session.version()
142 if (isOlder(base ?? version, MIN_VERSION)) {
143 out.push(`Warning: Claude Code ${version} is older than 2.1.251, so it sends no cache data. The line stays empty until you update.`)
144 }
145 return out
146}
147
148// The statusLine in force and the settings source it comes from, highest precedence first.
149async function inForce($: EngineInterface) {
150 for (const source of ['policy', 'flag', 'local', 'project', 'user'] as const) {
151 const settings = await $.settings.read({ source }).catch(() => ({}))
152 const statusLine = asStatusLine((settings as Record<string, unknown>).statusLine)
153 if (statusLine) return { source, statusLine }
154 }
155 return { source: null, statusLine: null }
156}
157
158// The file setup writes: where the status line lives, except that a project's
159// shared settings get a local override instead of a path from this machine.
160async function targetFile($: EngineInterface, source: string | null) {
161 const root = await $.session.root() // where Claude Code reads a project's settings from
162 if (source === 'local' || source === 'project') return `${root}/.claude/settings.local.json`
163 return `${await configDir($)}/settings.json`
164}
165
166async function configDir($: EngineInterface) {
167 const custom = await $.env.get('CLAUDE_CONFIG_DIR')
168 if (custom) return custom.replace(/\/+$/, '')
169 const home = await $.env.get('HOME')
170 if (!home) throw new Error('HOME is not set, so the Claude config folder is unknown.')
171 return `${home}/.claude`
172}
173
174async function clockDir($: EngineInterface) {
175 return `${await configDir($)}/cache-clock`
176}
177
178async function readJson($: EngineInterface, file: string): Promise<Record<string, unknown>> {
179 if (!(await $.fs.exists(file))) return {}
180 const text = await $.fs.read(file)
181 if (!text.trim()) return {}
182 let parsed: unknown
183 try {
184 parsed = JSON.parse(text)
185 } catch {
186 throw new Error(`${file} is not valid JSON, so it was left as it is.`)
187 }
188 if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error(`${file} does not hold a settings object.`)
189 return parsed as Record<string, unknown>
190}
191
192async function readSaved($: EngineInterface, path: string): Promise<Saved | null> {
193 const text = await $.fs.read(path).catch(() => null)
194 if (!text) return null
195 try {
196 const saved = JSON.parse(text)
197 return typeof saved?.file === 'string' ? (saved as Saved) : null
198 } catch {
199 return null
200 }
201}
202
203// A short stable name for a settings file's saved state: FNV-1a of its path, in hex.
204function keyFor(file: string) {
205 let hash = 0x811c9dc5
206 for (let i = 0; i < file.length; i++) hash = Math.imul(hash ^ file.charCodeAt(i), 0x01000193) >>> 0
207 return hash.toString(16).padStart(8, '0')
208}
209
210// Safe inside double quotes for sh.
211function shellQuoted(path: string) {
212 return path.replace(/["\\$`]/g, c => `\\${c}`)
213}
214
215function asStatusLine(value: unknown): StatusLine | null {
216 return value && typeof value === 'object' && !Array.isArray(value) ? (value as StatusLine) : null
217}
218
219function isOurs(statusLine: StatusLine | null) {
220 return typeof statusLine?.command === 'string' && statusLine.command.includes(MARK)
221}
222
223function isOlder(version: string, min: number[]) {
224 const parts = version.split(/[.-]/).slice(0, 3).map(Number)
225 if (parts.some(Number.isNaN)) return false
226 for (let i = 0; i < min.length; i++) {
227 if (parts[i] !== min[i]) return parts[i] < min[i]
228 }
229 return false
230}
231