SLOPSHOPPER

where-am-i

A live recap above the prompt: goal, doing now, waiting on you, next. /where for a longer one.

newbandguardcommandpromptmodel
★ 167v0.1.4MITupdated 2026-10-05hamzafer/claude-code-mods/mods/where-am-i
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · where-am-i
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /where ⎿ where-am-i: OK ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

<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>

🚀 Install

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.

🧩 The mods

👀 See what's happening

ModWhat it doesCommand
🛰️mission-controlLive map of agents, tool calls and the code they touch/mission
📊context-barYour context window as one stacked bar, a color per category, with token counts and where it compacts/context-bar
🌦️token-weatherContext fill from Clear to Compact soon, plus a prompt-cache countdown
⏱️cache-clockA 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-iGoal, doing now, waiting on you, next step/where
➡️next-steps2 or 3 likely next prompts after each turn, one key to draft one1 2 3, 0 hides
💰usage-meter5-hour and 7-day plan usage, the reset countdown and the session's cost
💳openai-balanceYour 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-radarOne live line per running subagent/radar
🔍review-watchOne 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-lanesWhether this session has a browser, and who holds it/browser
🕌prayer-timesThe current prayer and how long is left, the next one, and zawal. Computed on your computer, Hanafi or standard Asr/prayers
🎬replay-theaterSteps through the last turn's edits, one diff at a time/replay
📝md-previewRenders the Markdown files Claude edits like GitHub does, with before and after side by side. Needs Chrome and a terminal that shows images/md

🛡️ Guard your repo

ModWhat it doesCommand
💥blast-radiusHolds rm -r, force pushes and migrations, shows what they'd delete, cancels after 60 s with no answer

💸 Spend less

ModWhat it doesCommand
🔀switchboardPicks 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

🔧 My setup (fork and adapt)

These are built around my own tools and rules. Fork them and change the rules to yours.

ModWhat it doesCommand
👀glanceOne line with what needs you: next meeting, PRs, Linear issues, Slack DMs. Needs gh and the Google Calendar, Linear and Slack connectors/glance
🚦merge-gateHolds 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-guardEnforces my writing and git rules: rewrites em dashes, asks before --amend, unformatted pushes, emails and phone numbers in notes
💾session-saverSaves where you left off, shows it on resume. Needs unpause/park [note]

🎮 For fun (opt-in)

ModWhat it doesCommand
📱reelsYouTube Shorts while Claude works, pauses when it's done/reels
🐍snakeSnake while Claude works/snake
🎵now-playingWhat 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>

🛰️ Flagship: mission-control

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.

mission-control at 4x: two subagents and a logout feature landing 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, live
  • 🗺️ c shows the code map, with import arrows
  • 🔵 Blue while the agent reads a file
  • 🟠 Orange while it writes
  • 🟢 Green when done, with one line on what changed

The Code view also needs macOS, Google Chrome and a terminal that shows images (Ghostty, kitty, iTerm2). The Who view works everywhere.

📚 More

Source 2 files
hooks/register.tsx 164 lines
1// Where Am I: a live recap above the prompt (goal, now, waiting on you, next), plus /where.
2import { atom, read, update } from 'claude-code'
3import type { EngineInterface, Register } from 'claude-code'
4
5import type { Recap } from '../types'
6
7const MODEL = 'haiku'
8const MAX_LOG = 20
9
10// Held by the host, so the recap survives a hot reload of this file.
11const recap = atom({ plugin: 'where-am-i', key: 'recap' } as const, null as Recap | null)
12const live = atom({ plugin: 'where-am-i', key: 'live' } as const, '')
13// True while the next-steps mod shows its list of next prompts: this band leaves out its own next meanwhile.
14const nextStepsActive = { plugin: 'next-steps', key: 'active' } as const
15
16export const register: Register = on => {
17  let prompt = ''
18  let log: string[] = []
19
20  on('session.start', async ($, e, next) => {
21    const r = await next(e)
22    await $.command.register({ name: 'where', description: 'Recap the session so far in a few bullets' }).catch(() => {}) // a name Claude Code already has is refused: start anyway
23    return r
24  })
25
26  on('prompt.submit', async ($, e, next) => {
27    prompt = e.text.slice(0, 1500)
28    log = []
29    await update($, live, () => 'reading your message')
30    return next(e)
31  })
32
33  // Observe only: note what is happening, then let the call run untouched.
34  // Only Claude's own calls count: one another mod makes in the background (`$.tool.call`,
35  // `$.mcp.call`) is raised by that plugin, and `next.origin` names it instead of the engine.
36  on('tool.call', async ($, e, next) => {
37    if (next.origin.plugin !== 'engine') return next(e)
38    const line = describe(e as unknown as Record<string, unknown>)
39    log = [...log, e.agentId ? `(agent) ${line}` : line].slice(-MAX_LOG)
40    if (!e.agentId) await update($, live, () => line)
41    const r = await next(e)
42    return r
43  })
44
45  on('turn.complete', async ($, e, next) => {
46    const r = await next(e)
47    if (!e.agentId) {
48      await update($, live, () => '')
49      void summarize($, prompt, log, e.answer).catch(() => {}) // in the background, so the turn ends at once
50    }
51    return r
52  })
53
54  on('command.run', { command: 'where' }, async $ => ({ text: await longRecap($, prompt, log) }))
55
56  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
57    const rest = await next(e) // what other mods and Claude Code draw here stays
58    const r = await read($, recap)
59    if (e.props.hasSurvey || !r) return rest
60
61    const { Box, Text } = $.ui.resolve(e)
62    const now = clip((await read($, live)) || r.now)
63    const { value: hasNextSteps = false } = await $.state.get(nextStepsActive)
64    const nextStep = hasNextSteps ? '' : r.next
65
66    return (
67      <Box flexDirection="column">
68        <Box flexDirection="column" paddingX={1}>
69          <Text wrap="truncate-end">
70            <Text color="cyan" bold>{'◆ Goal: '}</Text>
71            <Text>{clip(r.goal)}</Text>
72          </Text>
73          <Text wrap="truncate-end">
74            <Text dimColor>{'  now: '}</Text>
75            <Text>{now}</Text>
76            {nextStep !== '' && <Text dimColor>{'  ·  next: '}</Text>}
77            {nextStep !== '' && <Text>{clip(nextStep)}</Text>}
78          </Text>
79          {r.waiting !== '' && (
80            <Text color="yellow" wrap="truncate-end">{`  waiting on you: ${clip(r.waiting)}`}</Text>
81          )}
82        </Box>
83        {rest}
84      </Box>
85    )
86  })
87}
88
89// A short label for one tool call: what a person would say Claude is doing.
90export function describe(e: Record<string, unknown>): string {
91  const tool = String(e.tool)
92  const s = (k: string) => (typeof e[k] === 'string' ? (e[k] as string) : '')
93  const file = (p: string) => p.split('/').slice(-2).join('/')
94  if (tool === 'Bash') return `running ${s('description') || s('command').slice(0, 60)}`
95  if (tool === 'Edit' || tool === 'Write') return `editing ${file(s('file_path'))}`
96  if (tool === 'Read') return `reading ${file(s('file_path'))}`
97  if (tool === 'Grep' || tool === 'Glob') return `searching for ${s('pattern').slice(0, 40)}`
98  if (tool === 'Agent') return `starting an agent: ${s('description')}`
99  if (tool === 'AskUserQuestion') return 'asking you a question'
100  if (tool.startsWith('mcp__')) return `using ${tool.split('__').slice(1).join(' ')}`
101  return `using ${tool}`
102}
103
104async function summarize($: EngineInterface, prompt: string, log: string[], answer: string) {
105  const before = await read($, recap)
106  const r = await $.model.complete({
107    model: MODEL,
108    maxTokens: 300,
109    system:
110      'You keep a one-glance recap of a coding session for someone with ADHD. Plain words, no em dashes, ' +
111      'each field at most 70 characters. Reply with JSON only: {"goal","now","waiting","next"}. ' +
112      '"goal": the overall aim of the session (keep the previous goal unless it clearly changed). ' +
113      '"now": what was just done. "waiting": what the assistant is waiting on from the person, or "". ' +
114      '"next": the next step.',
115    prompt: [
116      `Previous recap: ${before ? JSON.stringify(before) : 'none'}`,
117      `The person's latest message: ${prompt}`,
118      `Tools used this turn: ${log.join('; ') || 'none'}`,
119      `The assistant's reply: ${answer.slice(0, 2500)}`,
120    ].join('\n\n'),
121  })
122  if (!r.isAnswered) return
123  const parsed = parseRecap(r.text)
124  if (parsed) await update($, recap, () => parsed)
125}
126
127async function longRecap($: EngineInterface, prompt: string, log: string[]) {
128  const messages = (await $.session.messages()).slice(-12)
129  const r = await $.model.complete({
130    model: MODEL,
131    maxTokens: 500,
132    system:
133      'Write a recap of this coding session for someone who lost track. Plain words, no em dashes. ' +
134      'At most 6 short bullets: the goal, what is done, what is happening now, what is waiting on them, the next step.',
135    prompt: [
136      `<transcript>\n${messages.filter(m => m.text.trim() !== '').map(m => `[${m.role === 'user' ? 'person' : 'assistant'}] ${m.text.slice(0, 800)}`).join('\n')}\n</transcript>`,
137      `Latest message: ${prompt}`,
138      `Recent tool calls: ${log.join('; ') || 'none'}`,
139      'Write the recap of the transcript above now: the bullets only, not a reply to it.',
140    ].join('\n\n'),
141  })
142  return r.isAnswered ? r.text : 'Could not build a recap right now.'
143}
144
145// At most 70 characters, cut at a word: the model does not always keep to the limit.
146export function clip(text: string, max = 70) {
147  const first = text.split(/(?<=[.!?])\s/)[0] ?? text // one sentence
148  if (first.length <= max) return first.replace(/\.$/, '')
149  return `${first.slice(0, first.lastIndexOf(' ', max - 1))}…`
150}
151
152// The model's JSON, tolerating a code fence around it.
153export function parseRecap(text: string): Recap | null {
154  const body = text.slice(text.indexOf('{'), text.lastIndexOf('}') + 1)
155  try {
156    const o = JSON.parse(body) as Record<string, unknown>
157    const field = (k: string) => (typeof o[k] === 'string' ? clip((o[k] as string).replace(/\s*—\s*/g, ', ').trim()) : '')
158    if (!field('goal')) return null
159    return { goal: field('goal'), now: field('now'), waiting: field('waiting'), next: field('next') }
160  } catch {
161    return null
162  }
163}
164
types/index.d.ts 10 lines
1export type Recap = { goal: string; now: string; waiting: string; next: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'where-am-i': { recap: Recap | null; live: string }
6    // Owned by the next-steps mod, read here. Kept the same as its own contract.
7    'next-steps': { steps: { turnId: string; items: string[] } | null; active: boolean }
8  }
9}
10