SLOPSHOPPER

replay

/replay, or r after a turn, steps through its file edits as diffs above the prompt

newbandguardcommandprompt
★ 1v?MITupdated 2026-10-02aguiddir/claude-config/plugins/replay
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · replay
› 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 › /replay ⎿ replay: Aucune édition au dernier tour. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-config

My Claude Code setup, tuned for Opus 5.5 / Sonnet 5.5.

git clone git@github.com:aguiddir/claude-config.git ~/PycharmProjects/claude-config
cd ~/PycharmProjects/claude-config
./install.sh --dry-run   # see what would change
./install.sh             # apply; safe to re-run

Needs claude, python3 and git. npm is optional (Playwright CLI). Skip parts with --skip-plugins, --skip-mcp, --skip-playwright.

Just the desktop notifications

A notification when Claude finishes, fails, asks for a permission or asks a question. In herdr it names the workspace and the agent session, and stays quiet for the pane you are looking at. Each session keeps a single notification, replaced in place, that leaves the screen after a few seconds. Linux only (D-Bus notifications, tested on GNOME); needs jq.

/plugin marketplace add aguiddir/claude-config
/plugin install notify@claude-config

Titles are in French: edit the case in plugins/notify/scripts/notify.sh to change them.

Just the replay

After a turn that edited files, a band above the prompt offers to replay them: press r in an empty prompt, or type /replay any time. The edits show one diff at a time above the prompt; n and p step, q closes, the mouse wheel scrolls a long diff. Only Edit and Write calls are recorded, not changes made through Bash. Needs Claude Code 2.1.287 or later (mods).

/plugin marketplace add aguiddir/claude-config
/plugin install replay@claude-config

Messages are in French: edit the strings in plugins/replay/hooks/register.tsx to change them.

Just the CI watch

For Vidal repos built on jenkins.vidal.net. A band above the prompt follows the Jenkins build of the current branch, or of its PR (PR-<n>) when the branch has no job of its own: a progress bar from Jenkins' estimated duration, the stage running, each stage's state, and a link to the build. When the build ends, a toast gives its result. Once the build's own SonarQube stage has run, the band shows the quality gate of the branch or PR with the conditions that failed, read 30 seconds after the end so that it is this build's analysis, and a toast says when it fails. A finished build stays on the band for 15 minutes after it ends or after /ci, whichever is later, so /ci shows the build it finds however old. When the build fails or its gate is red, f in an empty prompt (until the next prompt you send; after that, ctrl+x tab then f, or the band's button) puts the failure in the prompt box, to read, edit and send yourself: the failed stage, the console's error lines and the gate's broken conditions. Sending that prompt attaches the last 150 lines of the console, out of the box.

It reads the repo and branch of the directory Claude Code runs in and polls Jenkins every 10 seconds, anonymously, from the Vidal network. /ci 231, /ci #231 or /ci <PR URL> follows that PR's job whatever the branch, /ci <path> follows another repo, /ci alone goes back to the session's directory; each answers with the build it found. Needs gh (to find the PR) and, for the quality gate, the SonarQube MCP server connected in Claude Code. Needs Claude Code 2.1.287 or later.

/plugin marketplace add aguiddir/claude-config
/plugin install ci-watch@claude-config

The Jenkins host and job folder (team.software/github) are set at the top of plugins/ci-watch/hooks/jenkins.ts.

Just the PR comments

A band above the prompt lists the open PRs of the repo that have unresolved review threads, in two groups, so it works from main too: those to work on (yours, and the current branch's whoever wrote it) and those you review (asked or already reviewed): 💬 à traiter : #89 (9) · en relecture : #73 (10). c in an empty prompt (while new threads have come in and no prompt has been sent since; otherwise /pr-review, or /pr-review 89 for one PR) opens them in a pane, one thread at a time: the code it is about, as a diff, and its whole conversation, badged ↩ répondu when the PR's author has the last word and obsolète when it is about older code. In the pane:

KeyDoes
n / pnext / previous thread
tnext PR, when several have threads
xmark the thread, for f
amark every thread, or none once they all are
fputs the marked threads (or the one shown) in the prompt box, to read, edit and send to Claude; once sent they are badged → Claude. When the PR is not on the current branch, the prompt tells Claude to check it out first (or use a worktree if changes are in progress). On your own PR it asks for one --fixup commit per commit corrected, then a push; on anyone else's, a local fix with no commit and no push
rwrites a reply, posted to GitHub on Enter; Esc gives it up and keeps the pane, as the field takes every letter meanwhile
vresolves the thread on GitHub
oopens the thread in the browser
q / Esccloses the pane

The keys work while the pane holds the keyboard; if it opened without it, ctrl+x tab or a click gives it. Replies and resolutions go through gh with your account, with no confirmation beyond the key. GitHub is asked every minute for the list of PRs (1 point of its 5000 an hour), and a PR's threads only when it changed, after a write to it, or every ten minutes in case someone else resolved one. Needs gh, logged in, and Claude Code 2.1.287 or later.

/plugin marketplace add aguiddir/claude-config
/plugin install pr-comments@claude-config

Just the explain-diff

Reading a diff is the slowest way to understand what an agent did. /explain-diff (or "explique-moi ce que tu as changé") publishes a private web page about the session's edits, a commit range (/explain-diff A^..B), a PR (/explain-diff #42) or a module (/explain-diff src/billing): what changed in one sentence, a before/after diagram of each changed flow, the files grouped by intent, the decisions made and the options not taken, the boundaries crossed (API, schema, config, security), and the open points. It runs no check and gives no verdict: the reader judges. A change that alters no behaviour (a rename, a config value, a bump) stays in the terminal, whatever its size. The page is in French, written at about 80% of ASD-STE100. After Karpathy's post on the output formats that are fastest to understand. Needs the Artifact tool (claude.ai login).

/plugin marketplace add aguiddir/claude-config
/plugin install explain-diff@claude-config

What's inside

PathInstalled asWhat it does
claude/CLAUDE.md~/.claude/CLAUDE.md (symlink)Global instructions: autonomy, long sessions, pre-review format
claude/rules/git-commit.md~/.claude/rules/ (symlink)Commit message rules: why over what, fixup commits for review changes
claude/statusline-command.sh~/.claude/ (symlink)3-line status line: repo/git/PR, context/cost, rate limits
claude/settings.jsonmerged into ~/.claude/settings.jsonAuto mode, status line, Opus effort, theme
plugins.txtclaude plugin installMarketplaces and plugins
plugins/notify/plugin notify@claude-configDesktop notification (D-Bus) per hook event: done, error, permission, question
plugins/replay/plugin replay@claude-config/replay (or r after a turn) steps through the last turn's Edit/Write calls as diffs above the prompt (a mod)
plugins/ci-watch/plugin ci-watch@claude-configBand above the prompt following the current branch's Jenkins build stage by stage, with its Sonar quality gate (a mod)
plugins/pr-comments/plugin pr-comments@claude-configBand and pane for the PR's unresolved review threads: send some to Claude, reply, resolve (a mod)
plugins/explain-diff/plugin explain-diff@claude-config/explain-diff: a private web page to understand a change before merging it (before/after diagrams, files by intent, decisions)

Symlinked files take effect as soon as you edit them here. settings.json is merged instead of linked because Claude Code rewrites it (/config, /model); existing keys and permission rules are kept, and the previous file is saved as .bak.<date>.

The installer also sets up:

  • codebase-memory-mcp: code knowledge-graph MCP server, from the release tarball after a SHA-256 check, registered for Claude Code only.
  • jq in ~/.local/bin, from the release binary after a SHA-256 check, when it is not already installed. The notify plugin needs it.
  • Playwright agent CLI with its skills, when npm is available.

Sources

Source 3 files
hooks/register.tsx 145 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Step } from '../types'
5import { createdHunk, toPatch } from './patch'
6import type { Hunk } from './patch'
7
8const pending = atom({ plugin: 'replay', key: 'pending' } as const, [])
9const steps = atom({ plugin: 'replay', key: 'steps' } as const, [])
10const at = atom({ plugin: 'replay', key: 'at' } as const, 0)
11// The band offering the replay, from a turn with edits to the next prompt.
12const hint = atom({ plugin: 'replay', key: 'hint' } as const, false)
13// The replay itself, drawn in the band above the prompt: a pane would dock
14// beside a fullscreen transcript, and its placement is not the mod's to pick.
15const isOpen = atom({ plugin: 'replay', key: 'isOpen' } as const, false)
16
17const openReplay = async ($: EngineInterface) => {
18  if ((await read($, steps)).length === 0) return false
19  await update($, at, () => 0)
20  await update($, hint, () => false)
21  await update($, isOpen, () => true)
22  return true
23}
24
25const go = async ($: EngineInterface, d: number) => {
26  const last = (await read($, steps)).length - 1
27  await update($, at, n => Math.max(0, Math.min(last, n + d)))
28}
29const close = ($: EngineInterface) => update($, isOpen, () => false)
30
31export const register: Register = on => {
32  on('session.start', async ($, e, next) => {
33    const r = await next(e)
34    await $.command.register({ name: 'replay', description: "Step through the last turn's file edits" })
35    return r
36  })
37
38  on('command.run', { command: 'replay' }, async $ => ({
39    text: (await openReplay($)) ? 'Replay des éditions du dernier tour.' : 'Aucune édition au dernier tour.',
40  }))
41
42  // Observes only: the edit runs as it would, its own patch is recorded.
43  on('tool.call', async ($, e, next) => {
44    const r = await next(e)
45    if ((e.tool !== 'Edit' && e.tool !== 'Write') || r.deny !== undefined || r.isError) return r
46    const result = r.result as { filePath: string; structuredPatch?: Hunk[]; type?: string; content?: string }
47    const hunks = result.structuredPatch?.length
48      ? result.structuredPatch
49      : result.type === 'create' && result.content !== undefined
50        ? [createdHunk(result.content)]
51        : []
52    if (hunks.length) {
53      const step: Step = { tool: e.tool, path: result.filePath, ...toPatch(hunks) }
54      await update($, pending, list => [...list, step])
55    }
56    return r
57  })
58
59  // turn.start is the main loop's alone; a subagent's edits join its turn.
60  on('turn.start', async ($, e, next) => {
61    await update($, pending, () => [])
62    return next(e)
63  })
64
65  on('turn.complete', async ($, e, next) => {
66    const r = await next(e)
67    const recorded = await read($, pending)
68    // Every main turn replaces the replay, an empty one included: /replay
69    // is the last turn's, never an older one's.
70    if (!e.agentId) {
71      await update($, steps, () => recorded)
72      if (recorded.length) await update($, hint, () => true)
73    }
74    return r
75  })
76
77  // A letter typed at the prompt never presses a band Button, so the keys are
78  // caught here, in an empty prompt only: `r` opens the replay from the hint,
79  // then `n`, `p` and `q` step and close it. None of them reach the box.
80  on('prompt.edit', async ($, e, next) => {
81    const key = e.text === '' ? e.inputText.toLowerCase() : ''
82    const consumed = { text: e.text, cursor: e.cursor }
83    if (await read($, isOpen)) {
84      if (key === 'n') return (await go($, 1), consumed)
85      if (key === 'p') return (await go($, -1), consumed)
86      if (key === 'q') return (await close($), consumed)
87    } else if (key === 'r' && (await read($, hint)) && (await openReplay($))) return consumed
88    return next(e)
89  })
90
91  // Any prompt sent takes the hint and the replay down.
92  on('prompt.submit', async ($, e, next) => {
93    await update($, hint, () => false)
94    await close($)
95    return next(e)
96  })
97
98  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
99    const list = await read($, steps)
100    if (e.props.hasSurvey || list.length === 0) return next(e)
101    const { Box, Text, Button, Code } = $.ui.resolve(e)
102
103    if (await read($, isOpen)) {
104      const i = Math.min(await read($, at), list.length - 1)
105      const step = list[i]!
106      return (
107        <Box flexDirection="column" borderStyle="round" borderColor="magenta">
108          <Text>
109            {list.map((_, n) => (
110              <Text color={n === i ? 'magenta' : undefined} dimColor={n !== i}>
111                {' '}
112                {n + 1}
113              </Text>
114            ))}
115            {'  '}
116            {step.tool} <Text dimColor>{step.path}</Text>
117          </Text>
118          {step.patch ? (
119            <Code format="diff" source={step.patch} path={step.path} />
120          ) : (
121            <Text dimColor>diff trop long pour s'afficher</Text>
122          )}
123          {step.omitted > 0 && <Text dimColor>… {step.omitted} bloc(s) non affichés</Text>}
124          <Box>
125            <Button key="prev" label="préc." hotkey="p" plain onPress={() => void go($, -1)} />
126            <Text> </Text>
127            <Button key="next" label="suiv." hotkey="n" plain onPress={() => void go($, 1)} />
128            <Text> </Text>
129            <Button key="close" label="fermer" hotkey="q" plain onPress={() => void close($)} />
130          </Box>
131        </Box>
132      )
133    }
134
135    if (!(await read($, hint))) return next(e)
136    return (
137      <Box>
138        <Text color="magenta">↺ {list.length} édition(s) au dernier tour · </Text>
139        <Button key="replay" label="rejouer" hotkey="r" plain onPress={() => void openReplay($)} />
140        <Text dimColor> (ou /replay)</Text>
141      </Box>
142    )
143  })
144}
145
hooks/patch.ts 26 lines
1export type Hunk = { oldStart: number; oldLines: number; newStart: number; newLines: number; lines: string[] }
2
3// `Code format="diff"` takes at most 10000 characters and refuses a hunk cut
4// in the middle, so whole hunks are kept while they fit; the rest is counted.
5const LIMIT = 10_000
6// Tab and newline are the only control characters Code accepts.
7const CONTROL = /[\x00-\x08\x0b-\x1f\x7f]/g
8
9export const toPatch = (hunks: readonly Hunk[]) => {
10  let patch = ''
11  let omitted = 0
12  for (const h of hunks) {
13    const text = `@@ -${h.oldStart},${h.oldLines} +${h.newStart},${h.newLines} @@\n${h.lines.join('\n')}`.replace(CONTROL, "")
14    const next = patch ? `${patch}\n${text}` : text
15    if (next.length > LIMIT) omitted++
16    else patch = next
17  }
18  return { patch, omitted }
19}
20
21// A Write that created a file may come back with no hunks: the whole content is added.
22export const createdHunk = (content: string): Hunk => {
23  const lines = content.replace(/\n$/, '').split('\n')
24  return { oldStart: 0, oldLines: 0, newStart: 1, newLines: lines.length, lines: lines.map(l => `+${l}`) }
25}
26
types/index.d.ts 9 lines
1// One file edit of a turn: the tool, the file, its unified-diff hunks.
2export type Step = { tool: string; path: string; patch: string; omitted: number }
3
4declare module 'claude-code' {
5  interface PluginState {
6    replay: { pending: Step[]; steps: Step[]; at: number; hint: boolean; isOpen: boolean }
7  }
8}
9