SLOPSHOPPER

gut-comments

Strips full-line narrating comments from Write/Edit before they land

newguardtoastmodel
v0.1.0no licenseupdated 2026-10-06invm/dotfiles/.claude/mods/gut-comments
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · gut-comments
› fix the failing auth test and add an audit log call ╭────────────────────────╮ │ gut-comments │ ⏺ Read(src/auth.ts) │ gut-comments: 1 gutted │ ⎿ 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 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Dotfiles

macOS. Run ./install.sh from repo root. Installs brew, packages from Brewfile, zsh, oh-my-zsh, tpm, stows configs.

Machine-specific shell config goes in ~/.zshrc.local (gitignored).

Insecure dir warning on macOS:

compaudit | xargs -I{} sudo chown -R $(whoami) "{}"
compaudit | xargs -I{} sudo chmod -R go-w "{}"
Source 2 files
hooks/register.ts 34 lines
1import type { Register, EngineInterface } from 'claude-code'
2import { candidates, drop, judgePrompt, parseKeep } from './strip'
3
4const gut = async ($: EngineInterface, path: string, src: string): Promise<{ out: string; removed: number }> => {
5  const cands = candidates(path, src)
6  if (!cands.length) return { out: src, removed: 0 }
7  const r = await $.model.complete({
8    model: 'haiku',
9    prompt: judgePrompt(src, cands),
10    maxTokens: 200,
11    effort: 'low',
12    timeoutMs: 10_000,
13  })
14  const keep = r.isAnswered ? parseKeep(r.text) : new Set<number>()
15  if (!r.isAnswered) $.ui.toast(`gut-comments: judge failed (${r.reason}), stripping blind`)
16  const kill = new Set(cands.filter(i => !keep.has(i)))
17  return { out: drop(src, kill), removed: kill.size }
18}
19
20export const register: Register = on => {
21  on('tool.call', { tool: 'Write' }, async ($, e, next) => {
22    const { out, removed } = await gut($, e.file_path, e.content)
23    if (removed) $.ui.toast(`gut-comments: ${removed} gutted`)
24    return next(removed ? { ...e, content: out } : e)
25  }).catch(($, e, next) => next(e))
26
27  on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
28    const { out, removed } = await gut($, e.file_path, e.new_string)
29    if (!removed || out === e.old_string) return next(e)
30    $.ui.toast(`gut-comments: ${removed} gutted`)
31    return next({ ...e, new_string: out })
32  }).catch(($, e, next) => next(e))
33}
34
hooks/strip.ts 48 lines
1const SLASH = /\.(ts|tsx|js|jsx|mjs|cjs|mts|cts|rs|go|java|kt|swift|c|h|cpp|cs|scala)$/
2const HASH = /\.(py|sh|zsh|bash|rb|yml|yaml|toml)$/
3const KEEP = /(eslint|biome-ignore|prettier-ignore|@ts-|TODO|FIXME|HACK|XXX|ponytail:|\/\/\/ <reference|noqa|type:|pyright|fmt:|nolint|shellcheck|SPDX|Copyright|License)/i
4
5export const candidates = (path: string, src: string): number[] => {
6  const prefix = SLASH.test(path) ? '//' : HASH.test(path) ? '#' : null
7  if (!prefix) return []
8  const out: number[] = []
9  let inBlock = false
10  for (const [i, line] of src.split('\n').entries()) {
11    const t = line.trim()
12    if (inBlock) {
13      out.push(i)
14      if (t.includes('*/')) inBlock = false
15      continue
16    }
17    if (i === 0 && t.startsWith('#!')) continue
18    if (KEEP.test(t)) continue
19    if (t.startsWith(prefix) && !t.startsWith('/**')) { out.push(i); continue }
20    if (prefix === '//' && t.startsWith('/*') && !t.startsWith('/**')) {
21      out.push(i)
22      inBlock = !t.includes('*/')
23    }
24  }
25  return out
26}
27
28export const drop = (src: string, lines: Set<number>): string =>
29  src.split('\n').filter((_, i) => !lines.has(i)).join('\n')
30
31export const judgePrompt = (src: string, cands: number[]): string => {
32  const lines = src.split('\n')
33  const numbered = lines.map((l, i) => `${cands.includes(i) ? '>' : ' '}${i}: ${l}`).join('\n')
34  return `Code below. Lines marked ">" are comments. Decide which to KEEP.
35KEEP only a comment that states something the code cannot express: a non-obvious invariant, a workaround with a reason or issue link, a gotcha, a why.
36DELETE comments that narrate what the code does, restate the next line, explain obvious logic, or justify a change to a reviewer.
37Also KEEP any marked line that is actually inside a string or template literal, not a real comment.
38Reply with ONLY a JSON array of line numbers to KEEP, e.g. [3,7]. Empty array if none.
39
40${numbered}`
41}
42
43export const parseKeep = (text: string): Set<number> => {
44  const m = text.match(/\[[\d,\s]*\]/)
45  if (!m) return new Set()
46  return new Set(JSON.parse(m[0]) as number[])
47}
48