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

My Claude Code setup: mods, plugins, skills and settings, so a new machine is one clone away.
git clone git@github-personal:aidan-nunn-sys/claude-config.git ~/Work/personal/claude-config
~/Work/personal/claude-config/install.sh
Then:
<SET-ME> values install.sh reports in ~/.claude.json (MCP secrets are never committed).~/.config/mise/config.toml: "npm:optimaizr" = { version = "0.11.0", allow_low_downloads = true }Mods and skills are symlinked from this repo, so edits land here directly. Settings changed through Claude Code live in ~/.claude; run ./capture.sh and review git diff to bring them back.
| Path | What |
|---|---|
mods/ | Mods loaded through CLAUDE_CODE_PLUGIN_DIRS (built by install.sh from this folder) |
plugins/plugins.json | Marketplaces and which marketplace plugins are enabled |
skills/ | Personal skills, symlinked into ~/.claude/skills |
settings/settings.json | ~/.claude/settings.json minus plugin dirs, plugins and hooks; ${HOME} for paths |
settings/mcp.json | User MCP servers, secrets blanked |
settings/rulebook.json | Generic example rules (copied only when the machine has none; not captured) |
docs/specs/ | Design specs |
| Mod | Source | Local changes |
|---|---|---|
| hud | own | prompt hint, context weather bar, cache countdown, kaomoji mascot |
| rulebook | own | hard rules in ~/.claude/rulebook.json, /rules, mcp__rulebook__add_rule |
| xagent | own | dispatch/route tools for Codex, Copilot, Antigravity |
| handoff | own | /handoff [next] writes a handoff doc to ~/.claude/handoffs/ |
| agent-radar | hamzafer/claude-code-mods @ 87f108479bbf | /radar runs mid-turn (immediate) and toggles closed; shows xagent's external agents |
| mission-control | hamzafer/claude-code-mods @ 87f108479bbf | shows xagent's external agents; code-map view (Haiku diff summaries, headless Chrome) stripped |
| blast-radius | hamzafer/claude-code-mods @ 87f108479bbf | 60 s auto-cancel |
| review-watch | hamzafer/claude-code-mods @ 87f108479bbf | none |
| where-am-i | hamzafer/claude-code-mods @ 87f108479bbf | shows only "next" |
| secret-box | ray-amjad/claude-code-mods @ 467b20300d8e | none |
| task-board | MarcoCarnevali/claude-code-mods @ a3e528dd00ce | /board (not /tasks), no auto-open, mid-turn + toggle |
Upstream commits are the repos' HEAD on 2026-10-14, when this repo was made; installs were from around then. Read upstream changes before pulling them in.
~/.claude/.credentials.json and MCP secrets.diagnose-crash, omarchy, symlinks into /usr/share/omarchy), themes/omarchy.json, the herdr SessionStart hook..claude-plugin/types/ in each mod: SDK types Claude Code writes itself.hooks/register.tsx 150 lines1// Where Am I: the next step above the prompt, plus /where for a full recap.
2// Local change: the band no longer shows goal, now or waiting on you.
3import { atom, read, update } from 'claude-code'
4import type { EngineInterface, Register } from 'claude-code'
5
6import type { Recap } from '../types'
7
8const MODEL = 'haiku'
9const MAX_LOG = 20
10
11// Held by the host, so the recap survives a hot reload of this file.
12const recap = atom({ plugin: 'where-am-i', key: 'recap' } as const, null as Recap | null)
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 return next(e)
30 })
31
32 // Observe only: note the call for the recap, then let it run untouched.
33 // Only Claude's own calls count: one another mod makes in the background (`$.tool.call`,
34 // `$.mcp.call`) is raised by that plugin, and `next.origin` names it instead of the engine.
35 on('tool.call', async ($, e, next) => {
36 if (next.origin.plugin !== 'engine') return next(e)
37 const line = describe(e as unknown as Record<string, unknown>)
38 log = [...log, e.agentId ? `(agent) ${line}` : line].slice(-MAX_LOG)
39 return next(e)
40 })
41
42 on('turn.complete', async ($, e, next) => {
43 const r = await next(e)
44 if (!e.agentId) {
45 void summarize($, prompt, log, e.answer).catch(() => {}) // in the background, so the turn ends at once
46 }
47 return r
48 })
49
50 on('command.run', { command: 'where' }, async $ => ({ text: await longRecap($, prompt, log) }))
51
52 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
53 const rest = await next(e) // what other mods and Claude Code draw here stays
54 const r = await read($, recap)
55 if (e.props.hasSurvey || !r) return rest
56
57 const { value: hasNextSteps = false } = await $.state.get(nextStepsActive)
58 if (hasNextSteps || r.next === '') return rest
59
60 const { Box, Text } = $.ui.resolve(e)
61 return (
62 <Box flexDirection="column">
63 <Box paddingX={1}>
64 <Text wrap="truncate-end">
65 <Text color="cyan" bold>{'◆ next: '}</Text>
66 <Text>{clip(r.next)}</Text>
67 </Text>
68 </Box>
69 {rest}
70 </Box>
71 )
72 })
73}
74
75// A short label for one tool call: what a person would say Claude is doing.
76export function describe(e: Record<string, unknown>): string {
77 const tool = String(e.tool)
78 const s = (k: string) => (typeof e[k] === 'string' ? (e[k] as string) : '')
79 const file = (p: string) => p.split('/').slice(-2).join('/')
80 if (tool === 'Bash') return `running ${s('description') || s('command').slice(0, 60)}`
81 if (tool === 'Edit' || tool === 'Write') return `editing ${file(s('file_path'))}`
82 if (tool === 'Read') return `reading ${file(s('file_path'))}`
83 if (tool === 'Grep' || tool === 'Glob') return `searching for ${s('pattern').slice(0, 40)}`
84 if (tool === 'Agent') return `starting an agent: ${s('description')}`
85 if (tool === 'AskUserQuestion') return 'asking you a question'
86 if (tool.startsWith('mcp__')) return `using ${tool.split('__').slice(1).join(' ')}`
87 return `using ${tool}`
88}
89
90async function summarize($: EngineInterface, prompt: string, log: string[], answer: string) {
91 const before = await read($, recap)
92 const r = await $.model.complete({
93 model: MODEL,
94 maxTokens: 300,
95 system:
96 'You keep a one-glance recap of a coding session for someone with ADHD. Plain words, no em dashes, ' +
97 'each field at most 70 characters. Reply with JSON only: {"goal","now","waiting","next"}. ' +
98 '"goal": the overall aim of the session (keep the previous goal unless it clearly changed). ' +
99 '"now": what was just done. "waiting": what the assistant is waiting on from the person, or "". ' +
100 '"next": the next step.',
101 prompt: [
102 `Previous recap: ${before ? JSON.stringify(before) : 'none'}`,
103 `The person's latest message: ${prompt}`,
104 `Tools used this turn: ${log.join('; ') || 'none'}`,
105 `The assistant's reply: ${answer.slice(0, 2500)}`,
106 ].join('\n\n'),
107 })
108 if (!r.isAnswered) return
109 const parsed = parseRecap(r.text)
110 if (parsed) await update($, recap, () => parsed)
111}
112
113async function longRecap($: EngineInterface, prompt: string, log: string[]) {
114 const messages = (await $.session.messages()).slice(-12)
115 const r = await $.model.complete({
116 model: MODEL,
117 maxTokens: 500,
118 system:
119 'Write a recap of this coding session for someone who lost track. Plain words, no em dashes. ' +
120 'At most 6 short bullets: the goal, what is done, what is happening now, what is waiting on them, the next step.',
121 prompt: [
122 `<transcript>\n${messages.filter(m => m.text.trim() !== '').map(m => `[${m.role === 'user' ? 'person' : 'assistant'}] ${m.text.slice(0, 800)}`).join('\n')}\n</transcript>`,
123 `Latest message: ${prompt}`,
124 `Recent tool calls: ${log.join('; ') || 'none'}`,
125 'Write the recap of the transcript above now: the bullets only, not a reply to it.',
126 ].join('\n\n'),
127 })
128 return r.isAnswered ? r.text : 'Could not build a recap right now.'
129}
130
131// At most 70 characters, cut at a word: the model does not always keep to the limit.
132export function clip(text: string, max = 70) {
133 const first = text.split(/(?<=[.!?])\s/)[0] ?? text // one sentence
134 if (first.length <= max) return first.replace(/\.$/, '')
135 return `${first.slice(0, first.lastIndexOf(' ', max - 1))}…`
136}
137
138// The model's JSON, tolerating a code fence around it.
139export function parseRecap(text: string): Recap | null {
140 const body = text.slice(text.indexOf('{'), text.lastIndexOf('}') + 1)
141 try {
142 const o = JSON.parse(body) as Record<string, unknown>
143 const field = (k: string) => (typeof o[k] === 'string' ? clip((o[k] as string).replace(/\s*—\s*/g, ', ').trim()) : '')
144 if (!field('goal')) return null
145 return { goal: field('goal'), now: field('now'), waiting: field('waiting'), next: field('next') }
146 } catch {
147 return null
148 }
149}
150types/index.d.ts 10 lines1export 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