SLOPSHOPPER

jr-done-check

Flags replies that claim something was tested or verified when nothing was run in the turn to check it: a warning under the message and a toast.

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

jr-done-check

Catches "tested", "verified", "all tests pass", "it works now" (or "testé", "vérifié", "ça marche") in a reply when nothing was run in that turn to check it.

⏺ I updated the checkout. Tested, it works now.
  ⚠ claims a check, but nothing was run in this turn to verify it
  • At the end of each turn, the reply's sentences are read for a claim of verification, in English or French. Negated ones ("not tested", "couldn't verify", "pas testé") are left out.
  • A claim counts as backed when the turn ran something that can check it: a shell command (Bash), or a tool whose name speaks of tests, a browser, a screenshot or an audit.
  • An unbacked claim gets a toast and a warning line under the block of the reply that holds it. The warning is drawn on screen only: the conversation and what Claude reads are unchanged.

Install

/plugin install jr-done-check --marketplace jrlprost/claude-code-mods

Access

Reads the tool names called in each turn and Claude's final reply. Draws a line under assistant messages. Never blocks anything, runs nothing, sends nothing over the network.

Source 3 files
hooks/register.tsx 50 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import { claims, isVerifying, keyOf, plain } from './claims'
5
6const flagged = atom({ plugin: 'jr-done-check', key: 'flagged' } as const, [])
7// Whether the main loop's current turn ran anything that can check a claim.
8const turn = { hasVerified: false }
9
10export const register: Register = on => {
11  on('turn.start', async ($, e, next) => {
12    if (e.agentId === undefined) turn.hasVerified = false
13    return next(e)
14  })
15
16  on('tool.call', async ($, e, next) => {
17    if (isVerifying(String(e.tool))) turn.hasVerified = true
18    return next(e)
19  }).catch(($, e, next) => next(e))
20
21  on('turn.complete', async ($, e, next) => {
22    const done = await next(e)
23    if (e.agentId !== undefined || turn.hasVerified) return done
24    const found = claims(e.answer ?? '')
25    if (found.length === 0) return done
26    await update($, flagged, list => [...(list ?? []), ...found.map(keyOf)].slice(-100))
27    $.ui.toast(`jr done-check · the reply says "${found[0]!.slice(0, 50)}" but nothing was run this turn to check it`)
28    return done
29  })
30
31  // Under the block of the reply that holds the claim: one warning line. The stored message is unchanged.
32  on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
33    const drawn = await next(e)
34    const keys = await read($, flagged)
35    if (keys.length === 0) return drawn
36    const text = plain(e.props.text)
37    if (!keys.some(k => text.includes(k))) return drawn
38    const { Box, Text } = $.ui.resolve(e)
39    return (
40      <Box flexDirection="column">
41        {drawn}
42        <Text>
43          <Text color="#ff8c42" bold>{'  ⚠ '}</Text>
44          <Text color="#ff8c42">claims a check, but nothing was run in this turn to verify it</Text>
45        </Text>
46      </Box>
47    )
48  })
49}
50
hooks/claims.ts 27 lines
1// A claim that something was checked, in English or French.
2// Letter-aware edges: \b does not see accented letters such as the é of "testé".
3const CLAIM =
4  /(?<![\p{L}])(tested|verified|validated|confirmed working|all tests pass(ed|ing)?|tests (are )?(green|passing)|(it|this|that) works( now)?|works now|testé(e|s)?|vérifié(e|s)?|validé(e|s)?|les tests passent|tests? au vert|ça marche|ça fonctionne|fonctionne (maintenant|bien|correctement))(?![\p{L}])/iu
5
6// Said of what was NOT done: "not tested", "couldn't verify", "pas testé", "non vérifié"…
7const NEGATED =
8  /\b(not|never|couldn'?t|could not|can'?t|cannot|unable to|without|untested|no way to|pas|jamais|non|sans|impossible de|pu)\s+(\w+\s+){0,2}$/i
9
10/** The sentences of `text` that claim a verification, negated ones left out. */
11export const claims = (text: string) =>
12  text
13    .split(/(?<=[.!?\n])\s+/)
14    .map(s => s.trim())
15    .filter(s => {
16      const m = CLAIM.exec(s)
17      return m !== null && !NEGATED.test(s.slice(0, m.index))
18    })
19
20/** A tool call that can check something: a shell command, a test runner, a browser or screenshot tool. */
21export const isVerifying = (tool: string) => tool === 'Bash' || /test|playwright|browser|screenshot|snapshot|lighthouse|a11y/i.test(tool)
22
23/** A short, stable key for a sentence, to find it again in the message as drawn. */
24export const keyOf = (sentence: string) => sentence.replace(/[*_`#>]/g, '').replace(/\s+/g, ' ').trim().slice(0, 60)
25
26export const plain = (text: string) => text.replace(/[*_`#>]/g, '').replace(/\s+/g, ' ')
27
types/index.d.ts 9 lines
1/** Keys of the claim sentences, in replies of this session, that nothing in their turn verified. */
2export type JrDoneCheckFlags = string[]
3
4declare module 'claude-code' {
5  interface PluginState {
6    'jr-done-check': { flagged: JrDoneCheckFlags }
7  }
8}
9