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.

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
console.log / debug / trace / dir, print(, debugger, dbg!(, var_dump(, fmt.Println(.TODO, FIXME, HACK, XXX./plugin install jr-nudges --marketplace jrlprost/claude-code-mods
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.
hooks/register.ts 52 lines1import 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}
52hooks/nudges.ts 45 lines1const 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