SLOPSHOPPER

jr-nudges

At the end of a turn, one toast when the code Claude wrote added debug statements (console.log, print, debugger) or TODO/FIXME/HACK lines.

newguardtoast
★ 2v0.1.0MITupdated 2026-10-15jrlprost/claude-code-mods/plugins/jr-nudges
A shopper browsing a rack in a slop shop
README

jr-nudges

At the end of a turn, one toast when the code Claude just wrote left something behind:

jr nudges · this turn added 2 debug statements (console.log, print, debugger…) and 1 TODO/FIXME/HACK
  • Debug statements: console.log / debug / trace / dir, print(, debugger, dbg!(, var_dump(, fmt.Println(.
  • Markers: TODO, FIXME, HACK, XXX.
  • Only what the turn added counts: each code file is compared with its text before the turn first touched it, so lines that were already there never trigger it.
  • Code files only (TypeScript, JavaScript, Python, Ruby, Go, Rust, Java, Kotlin, Swift, PHP, C/C++, C#, Scala, Dart, Lua, Vue, Svelte). Quiet when there is nothing to report.

Install

/plugin install jr-nudges --marketplace jrlprost/claude-code-mods

Access

Watches Write and Edit calls on code files and reads those files before and after the turn. Never blocks a call. Runs nothing, sends nothing over the network.

Source 2 files
hooks/register.ts 52 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3import { countChange, isCode, message } from './nudges'
4
5// The main loop's current turn: each code file written so far, with its text before the turn touched it.
6const turn = { files: new Map<string, string>() }
7
8const touchedPath = (tool: string, input: Record<string, unknown>) =>
9  tool === 'Write' || tool === 'Edit' || tool === 'MultiEdit' ? (typeof input.file_path === 'string' ? input.file_path : null) : null
10
11/** Debug statements and markers the turn added: each file now, against its text before the turn. */
12async function countTurn($: EngineInterface) {
13  let debug = 0
14  let markers = 0
15  for (const [file, before] of turn.files) {
16    const counts = countChange(before, await $.fs.read(file).catch(() => before))
17    debug += counts.debug
18    markers += counts.markers
19  }
20  return { debug, markers }
21}
22
23/** The file's text before this turn first touched it: empty for a new file. */
24async function snapshot($: EngineInterface, file: string) {
25  return (await $.fs.exists(file).catch(() => false)) ? await $.fs.read(file).catch(() => '') : ''
26}
27
28export const register: Register = on => {
29  on('turn.start', async ($, e, next) => {
30    if (e.agentId === undefined) turn.files = new Map()
31    return next(e)
32  })
33
34  on('tool.call', async ($, e, next) => {
35    const file = touchedPath(String(e.tool), e as unknown as Record<string, unknown>)
36    if (!file || !isCode(file)) return next(e)
37    const before = turn.files.get(file) ?? (await snapshot($, file))
38    const ran = await next(e)
39    if (!ran.isError && ran.deny === undefined && !turn.files.has(file)) turn.files.set(file, before)
40    return ran
41  }).catch(($, e, next) => next(e))
42
43  on('turn.complete', async ($, e, next) => {
44    const done = await next(e)
45    if (e.agentId !== undefined || turn.files.size === 0) return done
46    const { debug, markers } = await countTurn($)
47    const text = message(debug, markers)
48    if (text) $.ui.toast(text)
49    return done
50  })
51}
52
hooks/nudges.ts 45 lines
1const CODE = /\.(m?[jt]sx?|cjs|py|rb|go|rs|java|kt|swift|php|c|cc|cpp|h|hpp|cs|scala|dart|lua|vue|svelte)$/i
2
3export const isCode = (file: string) => CODE.test(file)
4
5const DEBUG = /\b(console\.(log|debug|trace|dir)\s*\(|debugger\b|dbg!\s*\(|\bprint\s*\(|var_dump\s*\(|fmt\.Println\s*\()/
6const MARKER = /\b(TODO|FIXME|HACK|XXX)\b/
7
8/** Counts debug statements and TODO-style markers on the lines a unified diff adds. */
9export const countAdded = (diff: string) => {
10  let debug = 0
11  let markers = 0
12  for (const line of diff.split('\n')) {
13    if (!line.startsWith('+') || line.startsWith('+++')) continue
14    const body = line.slice(1)
15    if (DEBUG.test(body)) debug++
16    if (MARKER.test(body)) markers++
17  }
18  return { debug, markers }
19}
20
21/** Counts in a new file, where every line is added. */
22export const countFile = (text: string) => countAdded(text.split('\n').map(l => `+${l}`).join('\n'))
23
24/** Lines of `after` that were not in `before` (as a multiset): what the turn added to the file. */
25export const addedLines = (before: string, after: string) => {
26  const left = new Map<string, number>()
27  for (const line of before.split('\n')) left.set(line, (left.get(line) ?? 0) + 1)
28  const added: string[] = []
29  for (const line of after.split('\n')) {
30    const n = left.get(line) ?? 0
31    if (n > 0) left.set(line, n - 1)
32    else added.push(line)
33  }
34  return added
35}
36
37export const countChange = (before: string, after: string) => countFile(addedLines(before, after).join('\n'))
38
39export const message = (debug: number, markers: number) => {
40  const parts: string[] = []
41  if (debug) parts.push(`${debug} debug statement${debug > 1 ? 's' : ''} (console.log, print, debugger…)`)
42  if (markers) parts.push(`${markers} TODO/FIXME/HACK`)
43  return parts.length ? `jr nudges · this turn added ${parts.join(' and ')}` : null
44}
45