A live line above the prompt for each running code review: Codex or a review subagent, its model, what it reviews, time, and Codex's latest output. A toast…

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 291 lines1// Review Watch: is my code review still running?
2// The band above the prompt has one live line per running review: a `codex review`
3// shell command or a subagent whose description says "review". It shows the model, what
4// is under review, the time, and Codex's latest output line. When a review ends a toast
5// says so, with the findings Codex reported, and its line shows a check for 30 s.
6import { atom, read, update } from 'claude-code'
7import type { EngineInterface, Register, Timer } from 'claude-code'
8
9import type { Review } from '../types'
10
11const SHOW_DONE_MS = 30_000 // a finished review stays in the band this long
12const BAND_ROWS = 4
13const POLL_MS = 3_000
14const UNSEEN_POLLS = 3 // a review whose process never shows up in ps is over after this many polls
15
16// Held by the host, so the reviews survive a hot reload of this file.
17const reviews = atom({ plugin: 'review-watch', key: 'reviews' } as const, [] as Review[])
18const now = atom({ plugin: 'review-watch', key: 'now' } as const, 0)
19
20let isPolling = false // a slow ps must not let two polls overlap
21
22export const register: Register = on => {
23 let cwd = ''
24 let timers: Timer[] = []
25
26 on('session.start', async ($, e, next) => {
27 const r = await next(e)
28 cwd = e.cwd
29 // A review agent that is no longer running (a resumed or new session) has no line to keep.
30 const live = new Set((await $.agent.list().catch(() => [])).filter(a => a.status === 'running').map(a => a.id))
31 await update($, reviews, list => list.filter(v => v.kind === 'codex' || v.status !== 'running' || live.has(v.id)))
32 for (const t of timers) t.cancel()
33 timers = []
34 // A clock for the elapsed times, ticking only while something shows.
35 timers.push($.clock.every(1000, () => {
36 void (async () => {
37 if ((await read($, reviews)).some(v => isShown(v, Date.now() - 3_000))) await update($, now, () => Date.now())
38 })().catch(() => {})
39 }))
40 timers.push($.clock.every(POLL_MS, () => void poll($).catch(() => {})))
41 return r
42 })
43
44 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
45 if (!isCodexReview(e.command)) return next(e)
46 const id = `codex-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`
47 const one: Review = {
48 id,
49 kind: 'codex',
50 model: modelOf(e.command) ?? (await configModel($)),
51 label: labelOf(e.command),
52 status: 'running',
53 startedAt: Date.now(),
54 key: keyOf(e.command),
55 out: outputOf(e.command, cwd, (await $.env.get('HOME').catch(() => undefined)) ?? ''),
56 polls: 0,
57 last: 'starting',
58 }
59 await update($, reviews, list => [one, ...list].slice(0, 20))
60 const r = await next(e)
61 if (r.deny || r.isError || !e.run_in_background) {
62 await finish($, id, Boolean(r.deny || r.isError)) // it ran in the foreground: it is over
63 } else if (!one.out) {
64 const path = /written to: (\S+\.output)/.exec(r.text ?? '')?.[1] // where Claude Code keeps a background shell's output
65 if (path) await update($, reviews, list => list.map(v => (v.id === id ? { ...v, out: path } : v)))
66 }
67 return r
68 })
69
70 on('agent.spawn', async ($, e, next) => {
71 const r = await next(e)
72 if (r.agentId && isReviewAgent(e.description ?? '')) {
73 const one: Review = { id: r.agentId, kind: 'agent', model: e.model || r.model || 'inherit', label: e.description, status: 'running', startedAt: Date.now(), last: e.subagentType }
74 await update($, reviews, list => [one, ...list.filter(v => v.id !== one.id)].slice(0, 20))
75 }
76 return r
77 })
78
79 on('turn.complete', async ($, e, next) => {
80 const r = await next(e)
81 if (e.agentId) await finish($, e.agentId, e.reason === 'error' || e.reason === 'aborted')
82 return r
83 })
84
85 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
86 const rest = await next(e) // what other mods and Claude Code draw here stays
87 await read($, now) // subscribes the band to the clock
88 const shown = (await read($, reviews)).filter(v => isShown(v))
89 if (e.props.hasSurvey || shown.length === 0) return rest
90 const { Box, Text } = $.ui.resolve(e)
91 return (
92 <Box flexDirection="column">
93 {shown.slice(0, BAND_ROWS).map(v => (
94 <Box flexDirection="column">
95 <Text wrap="truncate-end">
96 <Text color={color(v)} bold>{` ${icon(v)} ${v.kind === 'codex' ? 'codex' : 'agent'}`}</Text>
97 <Text>{` · ${v.model} · ${v.label}`}</Text>
98 {v.status !== 'running' && v.findings && <Text bold>{` · ${findingsText(v.findings)}`}</Text>}
99 <Text dimColor>{` ${elapsed(v)}`}</Text>
100 </Text>
101 {v.status === 'running' && v.kind === 'codex' && v.last !== '' && (
102 <Text dimColor wrap="truncate-end">{` └ ${v.last}`}</Text>
103 )}
104 </Box>
105 ))}
106 {shown.length > BAND_ROWS && <Text dimColor>{` +${shown.length - BAND_ROWS} more`}</Text>}
107 {rest}
108 </Box>
109 )
110 })
111}
112
113// Checks each running Codex review: is its process still there, and what did it print last.
114async function poll($: EngineInterface) {
115 if (isPolling) return
116 isPolling = true
117 try {
118 await pollOnce($)
119 } finally {
120 isPolling = false
121 }
122}
123
124async function pollOnce($: EngineInterface) {
125 const running = (await read($, reviews)).filter(v => v.kind === 'codex' && v.status === 'running')
126 if (running.length === 0) return
127 const ps = await $.process.run(['ps', '-axww', '-o', 'command='], { timeoutMs: 5_000 })
128 if (ps.exitCode !== 0) return
129 const lines = ps.stdout.split('\n').filter(l => isCodexReview(l)).map(plain)
130 for (const v of running) {
131 const isAlive = lines.some(l => `${l} `.includes(`${v.key ?? 'codex review'} `)) // whole arguments: `--base main` is not `--base main2`
132 const polls = (v.polls ?? 0) + 1
133 if (!isAlive && (v.seen || polls >= UNSEEN_POLLS)) {
134 await finish($, v.id, false)
135 continue
136 }
137 const last = v.out ? await lastLine($, v.out) : v.last
138 await update($, reviews, list => list.map(x => (x.id === v.id ? { ...x, polls, seen: x.seen || isAlive, last } : x)))
139 }
140}
141
142async function finish($: EngineInterface, id: string, failed: boolean) {
143 // Claimed in one update, so two callers (a poll and the tool call) never both toast.
144 let v: Review | undefined
145 await update($, reviews, list =>
146 list.map(x => (x.id === id && x.status === 'running' ? (v = { ...x, status: failed ? 'failed' : 'done', endedAt: Date.now() }) : x)),
147 )
148 if (!v) return
149 const done = v
150 const findings = done.kind === 'codex' && done.out && !failed ? findingsOf(await tail($, ['-c', '40000', '--', done.out])) : undefined
151 if (findings) await update($, reviews, list => list.map(x => (x.id === id ? { ...x, findings } : x)))
152 const what = done.kind === 'codex' ? `codex review (${done.model})` : `${done.label} (${done.model})`
153 const extra = findings ? ` · ${findingsText(findings)}` : ''
154 $.ui.toast(`${failed ? '✗' : '✓'} ${what} ${failed ? 'stopped' : 'done'} · ${elapsed(done)}${extra}`)
155}
156
157async function tail($: EngineInterface, args: string[]) {
158 const r = await $.process.run(['tail', ...args], { timeoutMs: 5_000 }).catch(() => null)
159 return r && r.exitCode === 0 ? r.stdout : ''
160}
161
162async function lastLine($: EngineInterface, file: string) {
163 const lines = clean(await tail($, ['-n', '30', '--', file])).split('\n').map(l => l.trim()).filter(Boolean)
164 return lines.at(-1)?.slice(0, 200) ?? ''
165}
166
167// The model from ~/.codex/config.toml, for a review run without one on its command line.
168async function configModel($: EngineInterface) {
169 const r = await $.process
170 .run(['sh', '-c', 'grep -E "^model *=" "$HOME/.codex/config.toml" | head -n 1'], { timeoutMs: 3_000 })
171 .catch(() => null)
172 return (r && /=\s*["']?([\w.:-]+)/.exec(r.stdout)?.[1]) || 'default'
173}
174
175// `codex review` where a command starts (after `;`, `&&`, `|`, `timeout 900` and the like), flags before `review` allowed.
176// Also the Codex plugin's `/codex:review` and `/codex:adversarial-review`: `node .../codex-companion.mjs review`.
177const CODEX_REVIEW =
178 /(?:^|[;&|(])\s*(?:(?:timeout|nohup|nice|env|time|command)(?:\s+[-\w.=:]+)*\s+)?(?:(?:\S*\/)?codex(?:\s+-{1,2}[\w-]+(?:[\s=]+(?!-)\S+)?)*\s+review|(?:\S*\/)?node\s+(?:"[^"]*|'[^']*|\S*)\/codex-companion\.mjs["']?\s+(?:adversarial-)?review)(?=\s|$)/
179
180export function isCodexReview(command: string) {
181 return CODEX_REVIEW.test(command)
182}
183
184// A subagent whose description says review (not "preview" or "interview").
185export function isReviewAgent(description: string) {
186 return /\breview(?:s|er|ers|ing)?\b/i.test(description)
187}
188
189const TITLE = /--title(?:\s+|=)(?:(["'])(.*?)\1|([^\s"';&|<>]+))/
190
191// The `codex ... review ...` part of a command, up to where the next command starts.
192function codexPart(command: string) {
193 const m = CODEX_REVIEW.exec(command)
194 if (!m) return ''
195 const rest = command.slice(m.index).replace(TITLE, '--title _')
196 return rest.split(/\s*(?:&&|\|\||;|\|(?!&))\s*/)[0]!
197}
198
199export function modelOf(command: string) {
200 const part = codexPart(command)
201 return /\s-c\s+["']?model\s*=\s*\\?["']?([\w.:-]+)/.exec(part)?.[1] ?? /[\s"'](?:--model|-m)(?:\s+|=)["']?([\w.:-]+)/.exec(part)?.[1]
202}
203
204export function labelOf(command: string) {
205 const title = titleOf(command)
206 if (title) return title
207 const part = codexPart(command)
208 const commit = /--commit(?:\s+|=)["']?(\w+)/.exec(part)?.[1]
209 if (commit) return `commit ${commit.slice(0, 7)}`
210 const base = /--base(?:\s+|=)["']?([^\s"']+)/.exec(part)?.[1]
211 return base ? `changes vs ${base}` : 'uncommitted changes'
212}
213
214function titleOf(command: string) {
215 const m = TITLE.exec(command.slice(Math.max(0, command.search(CODEX_REVIEW))))
216 return m ? (m[2] ?? m[3] ?? '') : ''
217}
218
219// What finds this review's process in ps: its arguments as ps prints them (quotes gone).
220export function keyOf(command: string) {
221 const m = CODEX_REVIEW.exec(command)
222 if (!m) return 'codex review'
223 const rest = command.slice(m.index + m[0].search(/codex(?:-companion\.mjs["']?)?\s/))
224 // Up to the first redirect or the next command, past a quoted title.
225 const masked = rest.replace(TITLE, x => x.replace(/[;&|<>]/g, '_'))
226 const cut = masked.search(/\s(?:\d?>|&>)|\s*(?:&&|\|\||;|\|)/)
227 return plain(cut >= 0 ? rest.slice(0, cut) : rest)
228}
229
230// The file the review's stdout goes to (`> file`): `~` from HOME, a relative one from a `cd` before it or the session's folder.
231export function outputOf(command: string, cwd: string, home = '') {
232 const at = Math.max(0, command.search(CODEX_REVIEW))
233 const re = /(\d|&)?>>?\s*(["']?)([^\s"'&|;<>]+)\2/g
234 const rest = command.slice(at).replace(TITLE, '--title _')
235 for (const m of rest.matchAll(re)) {
236 if (m[1] === '2' || m[3]!.startsWith('&')) continue
237 let path = m[3]!
238 if (path === '/dev/null' || path.includes('$')) return undefined
239 if (path.startsWith('~/')) {
240 if (!home) return undefined
241 path = home + path.slice(1)
242 }
243 if (path.startsWith('/')) return path
244 const cds = [...command.slice(0, at).matchAll(/(?:^|[;&|])\s*cd\s+(["']?)([^\s"';&|]+)\1/g)]
245 const dir = cds.at(-1)?.[2]
246 const base = dir ? (dir.startsWith('/') ? dir : dir.startsWith('~/') && home ? home + dir.slice(1) : `${cwd}/${dir}`) : cwd
247 return base ? `${base}/${path}` : undefined
248 }
249 return undefined
250}
251
252// Codex ends with a "Full review comments:" block of `- [P2] title — file:line` items.
253// Undefined when the output says neither (an error, a killed run, an unreadable file).
254export function findingsOf(text: string): string[] | undefined {
255 const at = text.lastIndexOf('Full review comments:')
256 if (at >= 0) return [...text.slice(at).matchAll(/^\s*-\s*\[(P\d)\]/gm)].map(m => m[1]!)
257 if (/\b(?:no (?:issues|findings|problems|bugs)|did not (?:find|identify)|didn't (?:find|identify)|looks good)\b/i.test(text.slice(-3000))) return []
258 return undefined
259}
260
261export function findingsText(findings: string[]) {
262 if (findings.length === 0) return 'no findings'
263 return `${findings.length} finding${findings.length === 1 ? '' : 's'} (${findings.join(', ')})`
264}
265
266export function isShown(v: Pick<Review, 'status' | 'endedAt'>, at = Date.now()) {
267 return v.status === 'running' || at - (v.endedAt ?? 0) < SHOW_DONE_MS
268}
269
270export function elapsed(v: Pick<Review, 'startedAt' | 'endedAt'>) {
271 const s = Math.max(0, Math.round(((v.endedAt ?? Date.now()) - v.startedAt) / 1000))
272 return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`
273}
274
275// A command line as ps prints it: no quotes, single spaces.
276function plain(text: string) {
277 return text.replace(/["']/g, '').replace(/\s+/g, ' ').trim()
278}
279
280function clean(text: string) {
281 return text.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '')
282}
283
284function icon(v: Review) {
285 return v.status === 'running' ? '⏳' : v.status === 'done' ? '✅' : '✗'
286}
287
288function color(v: Review) {
289 return v.status === 'running' ? 'cyan' : v.status === 'done' ? 'green' : 'red'
290}
291types/index.d.ts 22 lines1export type Review = {
2 id: string
3 kind: 'codex' | 'agent'
4 model: string
5 label: string
6 status: 'running' | 'done' | 'failed'
7 startedAt: number
8 endedAt?: number
9 key?: string // a Codex review: a string of its command line, to find its process
10 out?: string // a Codex review: the file its output goes to
11 seen?: boolean // a Codex review: its process has shown up in ps
12 polls?: number
13 last: string
14 findings?: string[] // a Codex review: P1, P2, ... per finding
15}
16
17declare module 'claude-code' {
18 interface PluginState {
19 'review-watch': { reviews: Review[]; now: number }
20 }
21}
22