Writing and git rules: replaces em dashes in prose, and asks before git commit --amend, an unformatted push, or personal info in notes and commits.

<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 182 lines1// rulebook-guard: enforces writing and git rules kept in CLAUDE.md and memory.
2// 1. No em dashes in prose: notes, docs, commit messages, PR text, Slack posts.
3// 2. New commits, not `git commit --amend`.
4// 3. Changed files formatted (ruff format, Prettier) before `git push`.
5// 4. No personal info (emails, phone numbers) in notes, memory or commits without asking.
6import type { EngineInterface, Register } from 'claude-code'
7
8const PROSE_FILE = /\.(md|mdx|markdown|txt)$/i
9const PROSE_COMMAND = /\bgh\s+(pr|issue)\s+(create|comment|edit|review)\b/
10// `git`, then any global options (`-c k=v`, `-C dir`, `--no-pager`), then the subcommand.
11const GIT = String.raw`\bgit(?:\s+(?:-[cC]\s+\S+|--?[\w-]+(?:=\S+)?))*\s+`
12const GIT_COMMIT = new RegExp(`${GIT}commit\\b`)
13const GIT_AMEND = new RegExp(`${GIT}commit\\b[^|;&]*--amend\\b`)
14const GIT_PUSH = new RegExp(`${GIT}push\\b`)
15const SLACK_POST = /^mcp__claude_ai_Slack__slack_(send_message|send_message_draft|schedule_message)$/
16const EMAIL = /[\w.+-]+@[\w-]+\.[a-z]{2,}/i
17const PHONE = /\+\d{1,3}[\s-]?\d{2,4}[\s-]?\d{2,4}[\s-]?\d{2,4}/
18const PRETTIER_FILE = /\.(js|jsx|ts|tsx|mjs|cjs|json|css|scss|md|mdx|ya?ml|html)$/
19
20export const register: Register = on => {
21 on('tool.call', { tool: 'Write' }, async ($, e, next) => {
22 if (await isPrivatePath($, e.file_path)) {
23 const found = pii(e.content)
24 if (found && !(await allow($, `This write to ${short(e.file_path)} contains what looks like ${found}.`))) {
25 return { deny: `rulebook-guard: the user blocked writing ${found} to ${e.file_path}. Personal info needs the user's approval first. Write the outcome instead of the details.` }
26 }
27 }
28 if (PROSE_FILE.test(e.file_path) && e.content.includes('—')) {
29 $.ui.toast(`rulebook-guard: replaced ${count(e.content)} em dash(es) in ${short(e.file_path)}`)
30 return next({ ...e, content: undash(e.content) })
31 }
32 return next(e)
33 })
34
35 on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
36 if (await isPrivatePath($, e.file_path)) {
37 const found = pii(e.new_string)
38 if (found && !(await allow($, `This edit to ${short(e.file_path)} adds what looks like ${found}.`))) {
39 return { deny: `rulebook-guard: the user blocked writing ${found} to ${e.file_path}. Personal info needs the user's approval first. Write the outcome instead of the details.` }
40 }
41 }
42 if (PROSE_FILE.test(e.file_path) && e.new_string.includes('—')) {
43 $.ui.toast(`rulebook-guard: replaced ${count(e.new_string)} em dash(es) in ${short(e.file_path)}`)
44 return next({ ...e, new_string: undash(e.new_string) })
45 }
46 return next(e)
47 })
48
49 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
50 let command = e.command
51 const shell = bare(command) // what the shell runs, without quoted text or heredoc bodies
52
53 if (GIT_AMEND.test(shell)) {
54 if (!(await allow($, 'This is `git commit --amend`. Your rule: make a new commit instead.'))) {
55 return { deny: 'rulebook-guard: the user blocked `git commit --amend`. Their rule is new commits, not amend. Make a new commit instead.' }
56 }
57 }
58
59 if (GIT_COMMIT.test(shell)) {
60 const found = pii(command)
61 if (found && !(await allow($, `This commit message contains what looks like ${found}.`))) {
62 return { deny: `rulebook-guard: the user blocked a commit message with ${found}. Personal info needs the user's approval first.` }
63 }
64 }
65
66 if (GIT_PUSH.test(shell)) {
67 const problems = await unformatted($, cdOf(command))
68 if (problems.length > 0 && !(await allow($, `Not formatted yet: ${problems.join('; ')}. Your rule: format before push.`))) {
69 return { deny: `rulebook-guard: the user blocked the push because these are not formatted: ${problems.join('; ')}. Run the formatter(s), commit, then push.` }
70 }
71 }
72
73 if ((GIT_COMMIT.test(shell) || PROSE_COMMAND.test(shell)) && command.includes('—')) {
74 $.ui.toast(`rulebook-guard: replaced ${count(command)} em dash(es) in the ${GIT_COMMIT.test(shell) ? 'commit message' : 'PR text'}`)
75 command = undash(command)
76 }
77
78 return next(command === e.command ? e : { ...e, command })
79 })
80
81 // Slack posts: no em dashes either.
82 on('tool.call', async ($, e, next) => {
83 if (!SLACK_POST.test(e.tool)) return next(e)
84 const args = e as unknown as { message?: unknown }
85 if (typeof args.message !== 'string' || !args.message.includes('—')) return next(e)
86 $.ui.toast(`rulebook-guard: replaced ${count(args.message)} em dash(es) in the Slack message`)
87 return next({ ...e, message: undash(args.message) } as typeof e)
88 })
89}
90
91// The command as the shell reads it: heredoc bodies and quoted strings blanked, so text
92// that merely mentions a command (a script, a commit message) is not taken for one.
93export function bare(command: string) {
94 return command
95 .replace(/<<-?\s*(['"]?)(\w+)\1[^\n]*\n[\s\S]*?\n\s*\2\s*(\n|$)/g, ' ')
96 .replace(/'[^']*'/g, "''")
97 .replace(/"(?:[^"\\]|\\.)*"/g, '""')
98}
99
100// "a — b" and "a—b" both read "a, b".
101export function undash(text: string) {
102 return text.replace(/\s*—\s*/g, ', ')
103}
104
105function count(text: string) {
106 return text.split('—').length - 1
107}
108
109// What kind of personal info the text seems to hold, by category only; never the value.
110export function pii(text: string): string | null {
111 if (EMAIL.test(text)) return 'an email address'
112 if (PHONE.test(text)) return 'a phone number'
113 return null
114}
115
116// Notes, auto-memory and CLAUDE.md files: where the PII rule applies.
117async function isPrivatePath($: EngineInterface, path: string) {
118 const home = (await $.env.get('HOME')) ?? ''
119 return (
120 (home !== '' && path.startsWith(`${home}/notes/`)) ||
121 /\/(\.claude|\.claude-work)\/projects\/[^/]+\/memory\//.test(path) ||
122 /(^|\/)CLAUDE\.md$/.test(path)
123 )
124}
125
126// Asks the person; anything but "Allow once" (or no one to ask) blocks.
127async function allow($: EngineInterface, why: string) {
128 try {
129 const answer = await $.ui.ask(`rulebook-guard: ${why} Allow it anyway?`, {
130 options: ['Block it', 'Allow once'],
131 header: 'Rulebook',
132 })
133 return answer === 'Allow once'
134 } catch {
135 return false
136 }
137}
138
139// The directory a leading `cd x &&` moves to, if any.
140export function cdOf(command: string): string | undefined {
141 return /^\s*cd\s+("[^"]+"|'[^']+'|\S+)\s*&&/.exec(command)?.[1]?.replace(/^["']|["']$/g, '')
142}
143
144// Changed files the repo's own formatters would change, as short reasons.
145async function unformatted($: EngineInterface, cwd: string | undefined): Promise<string[]> {
146 const init = cwd ? { cwd } : undefined
147 const top = await $.process.run(['git', 'rev-parse', '--show-toplevel'], init).catch(() => null)
148 if (!top || top.exitCode !== 0) return []
149 const root = top.stdout.trim()
150 const at = { cwd: root, timeoutMs: 60_000 }
151
152 let diff = await $.process.run(['git', 'diff', '--name-only', '--diff-filter=ACMR', '@{u}...HEAD'], at)
153 if (diff.exitCode !== 0) diff = await $.process.run(['git', 'diff', '--name-only', '--diff-filter=ACMR', 'origin/main...HEAD'], at)
154 if (diff.exitCode !== 0) return []
155 const files = diff.stdout.split('\n').filter(Boolean)
156 const problems: string[] = []
157
158 const py = files.filter(f => f.endsWith('.py'))
159 if (py.length > 0) {
160 const ruff = await $.process.run(['ruff', 'format', '--check', ...py], at).catch(() => null)
161 if (ruff && ruff.exitCode === 1) {
162 const bad = ruff.stdout.split('\n').filter(l => l.startsWith('Would reformat:')).map(l => l.slice(16).trim())
163 problems.push(`ruff format: ${bad.join(', ') || 'some Python files'}`)
164 }
165 }
166
167 const web = files.filter(f => PRETTIER_FILE.test(f))
168 const hasPrettier = await $.fs.exists(`${root}/node_modules/.bin/prettier`).catch(() => false)
169 if (web.length > 0 && hasPrettier) {
170 const prettier = await $.process.run([`${root}/node_modules/.bin/prettier`, '--check', ...web], at).catch(() => null)
171 if (prettier && prettier.exitCode === 1) {
172 const bad = `${prettier.stdout}\n${prettier.stderr}`.split('\n').filter(l => l.startsWith('[warn] ') && !l.includes('Code style issues')).map(l => l.slice(7).trim())
173 problems.push(`prettier: ${bad.join(', ') || 'some files'}`)
174 }
175 }
176 return problems
177}
178
179function short(path: string) {
180 return path.split('/').slice(-2).join('/')
181}
182