SLOPSHOPPER

reflect

/reflect: Claude critiques its own recent work (what may be wrong or missed, what to check next) in a pane, with one press to hand the follow-ups back.

newpaneguardcommandtoastmodel
v0.1.0no licenseupdated 2026-10-10gauravruhela07/claude-mods/reflect
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · reflect
│ ┃ Reflect ✕ › fix the failing auth test and add an audit log call │ ┃ Reflection 1 of 1 · 08:53 · running │ ┃ ⏺ Read(src/auth.ts) │ ┃ Reflecting… ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ [ Close ] ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ Auto-reflect after edits: off · /reflect aut ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /reflect │ ⎿ reflect: Reflecting on the recent work… │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Reflect
Reflection 1 of 1 · 08:53 · running Reflecting… [ Close ] Auto-reflect after edits: off · /reflect auto
README

claude-mods

Seven mods for Claude Code. Each one is a plugin of function hooks: it draws panes, status lines and toasts inside Claude Code, in the terminal and in the desktop app's Code tab.

ModWhat it doesHow to use it
savvy-progressA live band above the prompt while Claude works: what it is doing now ("Reading format.ts · 4s"), elapsed time, steps, files read and changed, errors, and a progress bar: the real task list when Claude keeps one, otherwise a time bar guessed from how long your past turns took (clearly labelled a guess). A one-line summary after each turn.Shows by itself. `/savvy-progress [on\off]`
skinsRe-skins the spinner (glyphs, verbs, color), the turn's closing line and the prompt hints. Skins: matrix, pirate, zen, retro, space. Your choice is remembered./skin (picker), /skin matrix, /skin off
filetreeA live file-tree pane of the project. Marks the files Claude read (R), changed (M) and created (A) this session, and opens the folders they live in./filetree, /filetree touched, /filetree all, /filetree close
cache-taxHow well the prompt cache works, and the "tax": the money paid above the ideal where every input token is a cache read. Status line cache 94% · tax $0.12, a per-turn table, and a toast with the likely cause when the cache busts./cache-tax, /cache-tax reset
blast-radiusFor each file Claude edits, the other files that import it; a flag on wide-reaching shell commands (git reset --hard, rm -rf, force push, ...). Shows only, never blocks./blast-radius, /blast-radius session, /blast-radius clear
reflectClaude critiques its own recent work: what may be wrong or missed, risky assumptions, what to check next. One button puts the follow-ups in your prompt box (it never sends them). Optional auto mode, off by default./reflect [focus], /reflect show, `/reflect auto on\off`
replay-theatreRecords the session and plays it back like a film: scene by scene, step by step, with play/pause, step, speed and a timeline./replay, /replay last, /replay copy; keys k play, j/l step, h/n scene, s speed, r restart

Install

In a Claude Code terminal session, install any mod by name:

/plugin install savvy-progress --marketplace gauravruhela07/claude-mods

Answer y to add the marketplace, then pick a scope (user scope loads it in every session). Repeat for each mod you want: skins, filetree, cache-tax, blast-radius, reflect, replay-theatre.

A mod installed from a terminal at the user scope also loads in the desktop app's Code tab.

Try one from a clone, without installing

git clone https://github.com/gauravruhela07/claude-mods
claude --plugin-dir claude-mods/filetree

Notes

  • Cost: only reflect calls the model, and only when you run /reflect or turn auto mode on. It asks over the session's own transcript, so most of it is served from the prompt cache.
  • cache-tax prices are first-party API list prices as of 2026-10-11, in cache-tax/hooks/prices.ts. Claude Code does not report 5-minute and 1-hour cache writes apart, so all writes use the 5-minute price.
  • blast-radius finds dependents with a text search (rg, falling back to grep) of import/require/from/include/use lines. It can miss aliased imports and Go's multi-line import blocks.
  • These mods use the Claude Code function-hooks API, which is early access and can change between releases. They were built and tested on Claude Code 2.1.29x.

Develop

Each mod folder holds .claude-plugin/plugin.json, hooks/ (the hooks module), types/index.d.ts (its state contract) and tests/.

claude plugin validate filetree
claude plugin test filetree
Source 3 files
hooks/register.tsx 297 lines
1import { atom, read, update } from 'claude-code'
2import type { ModelForkResult, Register, UiPressArgument } from 'claude-code'
3
4import type { Reflection } from '../types'
5import { clockTime, cost, failure, followUps, reflectPrompt, toUsage } from './text'
6
7const PANE = 'reflect'
8const TITLE = 'Reflect'
9const KEEP = 5
10const EDIT_TOOLS = new Set(['Edit', 'MultiEdit', 'Write', 'NotebookEdit'])
11const USAGE = 'Use /reflect [focus words], /reflect show, or /reflect auto [on|off]. Nothing changed.'
12
13const reflections = atom({ plugin: 'reflect', key: 'reflections' } as const, [])
14const shown = atom({ plugin: 'reflect', key: 'shown' } as const, null)
15const isAuto = atom({ plugin: 'reflect', key: 'isAuto' } as const, false)
16const hasEdited = atom({ plugin: 'reflect', key: 'hasEdited' } as const, false)
17
18/** The next reflection, running, after the ones in `list`. */
19const nextReflection = (list: readonly Reflection[], at: number, focus: string, origin: Reflection['origin']): Reflection => ({
20  id: (list[list.length - 1]?.id ?? 0) + 1,
21  at,
22  focus,
23  origin,
24  status: 'running',
25  markdown: '',
26  error: '',
27  usage: null,
28})
29
30const isRunning = (list: readonly Reflection[]) => list.some(one => one.status === 'running')
31
32export const register: Register = on => {
33  // Set in session.start: runs a reflection's fork from a timer of the session's own `$`,
34  // so no hook's dispatch waits on the model.
35  let runFork: ((job: Reflection) => void) | undefined
36
37  on('session.start', async ($, e, next) => {
38    runFork = job => {
39      $.clock.after(0, async () => {
40        let r: ModelForkResult | undefined
41        let thrown = ''
42        try {
43          r = await $.model.fork({ prompt: reflectPrompt(job.focus) })
44        } catch (err) {
45          thrown = `Could not run the reflection: ${err instanceof Error ? err.message : String(err)}`
46        }
47        const text = r?.isAnswered === true ? r.text.trim() : ''
48        const isDone = text !== ''
49        const error =
50          r === undefined
51            ? thrown
52            : r.isAnswered
53              ? isDone
54                ? ''
55                : 'The model replied without any text. Run /reflect to try again.'
56              : failure(r)
57        const usage = r === undefined ? null : toUsage(r)
58
59        await update($, reflections, list =>
60          list.map(
61            (one): Reflection =>
62              one.id === job.id ? { ...one, status: isDone ? 'done' : 'failed', markdown: text, error, usage } : one,
63          ),
64        )
65        if (job.origin === 'auto') {
66          $.ui.toast(isDone ? 'Reflection ready — /reflect show' : 'Reflection failed — /reflect show')
67        }
68      })
69    }
70
71    await $.command.register({
72      name: 'reflect',
73      description: 'Claude critiques its own recent work: what may be wrong or missed, and what to check next',
74      argumentHint: '[focus words | show | auto [on|off]]',
75      immediate: true,
76    })
77    const stored = await $.store.get('auto')
78    await update($, isAuto, () => stored === true)
79    // A reload drops a fork in flight with the old environment: say so rather than spin forever.
80    await update($, reflections, list =>
81      list.map(
82        (one): Reflection =>
83          one.status === 'running'
84            ? { ...one, status: 'failed', error: 'Stopped by a reload of the mod. Run /reflect to try again.' }
85            : one,
86      ),
87    )
88
89    return next(e)
90  })
91
92  on('command.run', { command: 'reflect' }, async ($, e) => {
93    const words = e.args.trim().split(/\s+/).filter(word => word !== '')
94    const first = (words[0] ?? '').toLowerCase()
95
96    if (first === 'auto') {
97      const arg = words.slice(1).join(' ').toLowerCase()
98      if (arg !== '' && arg !== 'on' && arg !== 'off') return { text: USAGE }
99
100      let isOn = false
101      await update($, isAuto, was => (isOn = arg === '' ? !was : arg === 'on'))
102      await $.store.set('auto', isOn)
103
104      return {
105        text: isOn
106          ? 'Auto-reflect is on: after a turn that edits files, Claude reflects in the background (this uses tokens). /reflect show opens it.'
107          : 'Auto-reflect is off.',
108      }
109    }
110
111    if (words.length === 1 && first === 'show') {
112      await update($, shown, () => null)
113      await $.ui.open({ id: PANE, title: TITLE })
114      const list = await read($, reflections)
115
116      return { text: list.length === 0 ? 'No reflection yet. Run /reflect to make one.' : 'Opened the newest reflection.' }
117    }
118
119    const focus = words.join(' ')
120    const list = await read($, reflections)
121    const isBusy = isRunning(list)
122
123    if (!isBusy) {
124      const job = nextReflection(list, await $.clock.now(), focus, 'command')
125      await update($, reflections, was => [...was, job].slice(-KEEP))
126      if (runFork === undefined) {
127        await update($, reflections, was =>
128          was.map(
129            (one): Reflection =>
130              one.id === job.id ? { ...one, status: 'failed', error: 'Reflect is still starting. Run /reflect again in a moment.' } : one,
131          ),
132        )
133      } else {
134        runFork(job)
135      }
136    }
137    await update($, shown, () => null)
138    await $.ui.open({ id: PANE, title: TITLE })
139
140    if (isBusy) return { text: 'A reflection is already running; it shows in the Reflect pane.' }
141
142    return { text: focus === '' ? 'Reflecting on the recent work…' : `Reflecting on the recent work (focus: ${focus})…` }
143  })
144
145  on('turn.start', async ($, e, next) => {
146    await update($, hasEdited, () => false)
147
148    return next(e)
149  })
150
151  on('tool.call', async ($, e, next) => {
152    const ran = await next(e)
153    const isEdit = e.agentId === undefined && EDIT_TOOLS.has(String(e.tool))
154
155    if (isEdit && ran.deny === undefined && ran.isError !== true && !(await read($, hasEdited))) {
156      await update($, hasEdited, () => true)
157    }
158
159    return ran
160  }).catch(($, e, next) => next(e))
161
162  on('turn.complete', async ($, e, next) => {
163    if (e.agentId !== undefined) return next(e)
164
165    const isEdited = await read($, hasEdited)
166    if (isEdited) await update($, hasEdited, () => false)
167    if (isEdited && !e.isAborted && runFork !== undefined && (await read($, isAuto))) {
168      const list = await read($, reflections)
169      const newest = list[list.length - 1]
170
171      if (!isRunning(list)) {
172        const job = nextReflection(list, await $.clock.now(), '', 'auto')
173        // Keep the pane on what the person reads; /reflect show jumps to the new one.
174        if (newest !== undefined && (await read($, shown)) === null) await update($, shown, () => newest.id)
175        await update($, reflections, was => [...was, job].slice(-KEEP))
176        runFork(job)
177      }
178    }
179
180    return next(e)
181  })
182
183  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
184    const { Box, Text, Button, Markdown } = $.ui.resolve(e)
185    const list = await read($, reflections)
186    const pick = await read($, shown)
187    const auto = await read($, isAuto)
188    const width = Math.max(20, e.props.bodyColumns)
189
190    const found = pick === null ? -1 : list.findIndex(one => one.id === pick)
191    const at = found < 0 ? list.length - 1 : found
192    const r = list[at]
193
194    const close = () => $.ui.close({ id: PANE })
195    const footer = (
196      <Box key="footer">
197        <Text dimColor wrap="wrap">
198          Auto-reflect after edits: {auto ? 'on' : 'off'} · /reflect auto
199        </Text>
200      </Box>
201    )
202
203    if (r === undefined) {
204      return (
205        <Box flexDirection="column" width={width} gap={1}>
206          <Box key="body">
207            <Text wrap="wrap">No reflection yet. Run /reflect (optionally with focus words) to make one.</Text>
208          </Box>
209          <Box key="actions" flexDirection="row">
210            <Button key="close" label="Close" hotkey="x" role="dismiss" onPress={close} />
211          </Box>
212          {footer}
213        </Box>
214      )
215    }
216
217    const parts = [`Reflection ${at + 1} of ${list.length}`, clockTime(r.at)]
218    if (r.status === 'running') parts.push('running')
219    if (r.usage !== null) parts.push(cost(r.usage))
220    if (r.origin === 'auto') parts.push('auto')
221    const head = parts.join(' · ')
222
223    const fix = async (press: UiPressArgument) => {
224      const text = followUps(r.markdown)
225      const box = await $.prompt.read()
226      const isEmpty = box.text.trim() === ''
227      const filled = await $.prompt.fill(isEmpty ? { text, mode: 'replace' } : { text: `\n\n${text}`, mode: 'append' })
228      if (filled.isFilled) {
229        $.ui.toast('The follow-ups are in the prompt box: edit them, then press Enter.')
230        return
231      }
232      const copied = await $.ui.copy({ text, surface: press.surface })
233      $.ui.toast(
234        copied.isCopied
235          ? 'The prompt box could not take the follow-ups, so they were copied: paste them in.'
236          : 'The prompt box could not take the follow-ups.',
237      )
238    }
239
240    const copy = async (press: UiPressArgument) => {
241      const copied = await $.ui.copy({ text: r.markdown, surface: press.surface })
242      $.ui.toast(copied.isCopied ? 'Reflection copied.' : 'Could not copy the reflection.')
243    }
244
245    const older = list[at - 1]
246    const newer = list[at + 1]
247    const isLast = at + 1 === list.length - 1
248
249    return (
250      <Box flexDirection="column" width={width} gap={1}>
251        <Box key="header" flexDirection="column">
252          <Text bold wrap="truncate-end">
253            {head}
254          </Text>
255          {r.focus !== '' && (
256            <Text dimColor wrap="truncate-end">
257              Focus: {r.focus}
258            </Text>
259          )}
260        </Box>
261        {r.status === 'running' && (
262          <Box key="body">
263            <Text color="claude">Reflecting…</Text>
264          </Box>
265        )}
266        {r.status === 'done' && <Markdown key="body" text={r.markdown} />}
267        {r.status === 'failed' && (
268          <Box key="body">
269            <Text color="warning" wrap="wrap">
270              {r.error}
271            </Text>
272          </Box>
273        )}
274        <Box key="actions" flexDirection="row" flexWrap="wrap" columnGap={1}>
275          {r.status === 'done' && (
276            <Button key="fix" label="Ask Claude to fix these" hotkey="f" variant="primary" onPress={fix} />
277          )}
278          {r.status === 'done' && <Button key="copy" label="Copy" hotkey="c" onPress={copy} />}
279          {older !== undefined && (
280            <Button key="older" label="← Older" hotkey="o" onPress={() => update($, shown, () => older.id)} />
281          )}
282          {newer !== undefined && (
283            <Button
284              key="newer"
285              label="Newer →"
286              hotkey="n"
287              onPress={() => update($, shown, () => (isLast ? null : newer.id))}
288            />
289          )}
290          <Button key="close" label="Close" hotkey="x" role="dismiss" onPress={close} />
291        </Box>
292        {footer}
293      </Box>
294    )
295  })
296}
297
hooks/text.ts 131 lines
1import type { ModelForkResult } from 'claude-code'
2
3import type { Usage } from '../types'
4
5/** The question the fork answers, over the session's own transcript. */
6export const reflectPrompt = (focus: string): string => {
7  const lines = [
8    'Stop here and review the work you have done in this session so far, as a skeptical senior reviewer would.',
9    'Do not use any tools and do not continue the task. Answer only with the review below.',
10    '',
11    'Reply in short Markdown with exactly these five headings, in this order, and nothing before or after them:',
12    '',
13    '## Asked',
14    'One or two sentences: what the person actually asked for, in their own terms.',
15    '',
16    '## Done',
17    'At most 5 bullets: what you actually changed, ran or produced. Name files, functions and commands. Facts only.',
18    '',
19    '## Might be wrong or missed',
20    'At most 6 bullets. Concrete problems: bugs, unhandled edge cases, parts of the request skipped or half done,',
21    'tests or checks not run, errors you glossed over, claims you made without verifying them.',
22    'Name the file and the function (or line) for each one.',
23    '',
24    '## Risky assumptions',
25    'At most 4 bullets: what you assumed without checking (an API, a version, a path, how something behaves,',
26    'what the person meant) that would break the work if it were false.',
27    '',
28    '## Check next',
29    'At most 5 numbered steps that would confirm or refute the points above. Each one is a command to run',
30    'or a concrete look (which file, what to look for in it). No vague advice such as "test thoroughly".',
31    '',
32    'Rules:',
33    '- Be honest and skeptical. Assume there is at least one mistake and look for it. Do not praise the work or reassure.',
34    '- Do not repeat or summarise the transcript beyond the two short opening sections.',
35    '- When a section truly has nothing, write "Nothing found" under its heading. Do not invent problems to fill it.',
36    '- No preamble and no closing remarks.',
37  ]
38  const words = focus.trim()
39  if (words !== '') lines.push('', `Focus on: ${words}.`)
40
41  return lines.join('\n')
42}
43
44const HEADING = /^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$/
45
46/** The body of the section whose heading matches `name`, without its heading; undefined when absent. */
47export const section = (markdown: string, name: RegExp): string | undefined => {
48  const lines = markdown.split('\n')
49  const start = lines.findIndex(line => {
50    const m = HEADING.exec(line)
51    return m !== null && name.test(m[2] ?? '')
52  })
53  if (start < 0) return undefined
54  const level = (HEADING.exec(lines[start] ?? '')?.[1] ?? '##').length
55  const body: string[] = []
56  for (const line of lines.slice(start + 1)) {
57    const m = HEADING.exec(line)
58    if (m !== null && (m[1] ?? '').length <= level) break
59    body.push(line)
60  }
61
62  return body.join('\n').trim()
63}
64
65export const MISSED = /^might be wrong or missed\b/i
66export const CHECK = /^check next\b/i
67
68/** What the fix button puts in the prompt box. */
69export const followUps = (markdown: string): string => {
70  const missed = section(markdown, MISSED)
71  const check = section(markdown, CHECK)
72  const intro = 'Please address these points from your reflection:\n\n'
73  if (missed === undefined && check === undefined) return intro + markdown.trim()
74  const parts: string[] = []
75  if (missed !== undefined) parts.push(`## Might be wrong or missed\n${missed || 'Nothing found'}`)
76  if (check !== undefined) parts.push(`## Check next\n${check || 'Nothing found'}`)
77
78  return intro + parts.join('\n\n')
79}
80
81/** Why a reflection has no text, in plain words. */
82export const failure = (r: ModelForkResult): string => {
83  if (r.isAnswered) return ''
84  switch (r.reason) {
85    case 'nothing-to-fork':
86      return 'Nothing to reflect on yet — do some work first.'
87    case 'aborted':
88      return 'The reflection was stopped before it finished. Run /reflect to try again.'
89    case 'empty-reply':
90      return 'The model replied without any text. Run /reflect to try again.'
91    case 'api-error': {
92      const status = r.status === null ? 'no response arrived' : `status ${r.status}`
93      return `The API returned an error (${status}, ${r.error}). Try /reflect again in a moment.`
94    }
95  }
96}
97
98/** What the fork cost; null when it cost nothing (nothing to fork, a refused request). */
99export const toUsage = (r: ModelForkResult): Usage | null => {
100  if (!r.isAnswered && r.reason === 'nothing-to-fork') return null
101  const u = r.usage
102  const usage = {
103    input: u.input_tokens,
104    output: u.output_tokens,
105    cacheRead: u.cache_read_input_tokens,
106    cacheWrite: u.cache_creation_input_tokens,
107  }
108
109  return usage.input + usage.output + usage.cacheRead + usage.cacheWrite > 0 ? usage : null
110}
111
112const short = (n: number): string =>
113  n >= 999_500 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)
114
115/** `1.2k tokens (89% from cache)`. */
116export const cost = (u: Usage): string => {
117  const total = u.input + u.output + u.cacheRead + u.cacheWrite
118  const prompt = u.input + u.cacheRead + u.cacheWrite
119  const cached = prompt > 0 ? Math.round((u.cacheRead / prompt) * 100) : 0
120
121  return `${short(total)} tokens (${cached}% from cache)`
122}
123
124/** `14:02`, local time where the environment knows it. */
125export const clockTime = (ms: number): string => {
126  const d = new Date(ms)
127  const pad = (n: number) => String(n).padStart(2, '0')
128
129  return `${pad(d.getHours())}:${pad(d.getMinutes())}`
130}
131
types/index.d.ts 39 lines
1/** What one reflection cost, from the fork result's `usage`. */
2export type Usage = {
3  input: number
4  output: number
5  cacheRead: number
6  cacheWrite: number
7}
8
9/** One reflection: asked by the person (`command`) or run after an editing turn (`auto`). */
10export type Reflection = {
11  id: number
12  /** When it started, ms since the epoch. */
13  at: number
14  /** Focus words given to /reflect, '' when none. */
15  focus: string
16  origin: 'command' | 'auto'
17  status: 'running' | 'done' | 'failed'
18  /** The model's Markdown once done, '' otherwise. */
19  markdown: string
20  /** Why it failed, in plain words, '' otherwise. */
21  error: string
22  usage: Usage | null
23}
24
25declare module 'claude-code' {
26  interface PluginState {
27    reflect: {
28      /** The last 5 reflections, oldest first. */
29      reflections: Reflection[]
30      /** The id the pane shows; null follows the newest. */
31      shown: number | null
32      /** Auto mode, mirrored from $.store key `auto`. */
33      isAuto: boolean
34      /** A main-loop edit succeeded in the turn that runs now. */
35      hasEdited: boolean
36    }
37  }
38}
39