SLOPSHOPPER

french-guard

Toasts when the last answer looks English or is French written without accents; /french-guard off|on|status

newcommandtoast
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · french-guard
› 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 › /french-guard ⎿ french-guard: french-guard on; last check: ok ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

french-guard

Catches answers that drift out of properly accented French, for users who expect French replies.

What it does

  • After each main-agent answer, shows a toast when:
  • the answer looks English (at least 40 words, and English stopwords make up 65% or more of English plus French stopwords), or
  • it contains 3 or more French words written without their accents (deja, probleme, resultat…; the toast shows up to 4 examples).
  • Code blocks, inline code, URLs, paths and file names are ignored.
  • /french-guard status reports the outcome of the last check.

Commands

CommandEffect
/french-guard off / onDisable / enable (default on)
/french-guard statusShows on/off and the last check result

Install

claude --plugin-dir /path/to/ModsTools/mods/french-guard

Limits

  • A heuristic based on stopword ratios and a list of about 50 unaccented words: it can miss cases or flag false positives.
  • It only warns; it does not rewrite or block anything.

Develop

claude plugin validate mods/french-guard
claude plugin test mods/french-guard   # 10 tests
Source 3 files
hooks/register.ts 42 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { messages } from './detect'
5
6const guard = atom({ plugin: 'french-guard', key: 'guard' } as const, { isEnabled: true, last: 'no answer checked yet' })
7
8async function answer($: EngineInterface, arg: string): Promise<string> {
9  if (arg === 'off' || arg === 'on') {
10    await update($, guard, g => ({ ...g, isEnabled: arg === 'on' }))
11
12    return `french-guard ${arg}.`
13  }
14  if (arg === 'status' || arg === '') {
15    const g = await read($, guard)
16
17    return `french-guard ${g.isEnabled ? 'on' : 'off'}; last check: ${g.last}`
18  }
19
20  return 'Usage: /french-guard off|on|status'
21}
22
23export const register: Register = on => {
24  on('session.start', async ($, e, next) => {
25    await $.command.register({ name: 'french-guard', description: 'french-guard: off|on|status' })
26
27    return next(e)
28  })
29
30  on('turn.complete', async ($, e, next) => {
31    if (e.agentId === undefined && (await read($, guard)).isEnabled) {
32      const found = messages(e.answer)
33      for (const text of found) $.ui.toast(text)
34      await update($, guard, g => ({ ...g, last: found.length === 0 ? 'ok' : found.join('; ') }))
35    }
36
37    return next(e)
38  })
39
40  on('command.run', { command: 'french-guard' }, async ($, e) => ({ text: await answer($, e.args.trim()) }))
41}
42
hooks/detect.ts 62 lines
1export const MIN_WORDS = 40
2export const ENGLISH_RATIO = 0.65
3export const MIN_UNACCENTED = 3
4
5const ENGLISH = new Set(
6  'the and is are was were with that this for to of it you have has not be will can which from but if then when what there their would should could been your we they i by an'.split(' '),
7)
8const FRENCH = new Set(
9  'le la les un une des du de et est sont avec que qui pour dans pas ce cette ces il elle nous vous je ne se au aux sur par mais où ou en ça été être très je tu ils elles'.split(' '),
10)
11// Frequent French words written without their accent; none is a valid French or common English word.
12const UNACCENTED = new Set(
13  `deja etre tres voila apres probleme problemes cree crees verifie verifies verifier reponse reponses numero numeros
14   electricite etape etapes ecran fenetre fenetres francais resultat resultats systeme systemes evenement evenements
15   creer genere generer generes memoire regle regles premiere derniere ecrire ecrit developpeur deploye specifique
16   specifiques theorie ca`.split(/\s+/),
17)
18
19export function stripNonProse(text: string): string {
20  return text
21    .replace(/```[\s\S]*?(```|$)/g, ' ')
22    .replace(/~~~[\s\S]*?(~~~|$)/g, ' ')
23    .replace(/`[^`\n]*`/g, ' ')
24    .replace(/\bhttps?:\/\/\S+/g, ' ')
25    .replace(/\S*\/\S*/g, ' ')
26    .replace(/\b[\w-]+\.(?:ts|tsx|js|json|md|swift|py|sh|yml|yaml|toml|html|css|txt|plist)\b/g, ' ')
27}
28
29export function words(prose: string): string[] {
30  return prose.toLowerCase().match(/[a-zàâäçéèêëîïôöùûüÿœ]+(?:'[a-zàâäçéèêëîïôöùûüÿœ]+)?/g) ?? []
31}
32
33export type Verdict = { isEnglish: boolean; unaccented: string[]; wordCount: number }
34
35export function analyze(answer: string): Verdict {
36  const all = words(stripNonProse(answer))
37  let en = 0
38  let fr = 0
39  const unaccented: string[] = []
40  for (const w of all) {
41    const bare = w.includes("'") ? (w.split("'")[1] ?? w) : w
42    if (ENGLISH.has(w)) en += 1
43    else if (FRENCH.has(w) || FRENCH.has(bare)) fr += 1
44    if (UNACCENTED.has(bare)) unaccented.push(bare)
45  }
46  const isEnglish = all.length >= MIN_WORDS && en + fr > 0 && en / (en + fr) >= ENGLISH_RATIO
47
48  return { isEnglish, unaccented, wordCount: all.length }
49}
50
51export function messages(answer: string): string[] {
52  const v = analyze(answer)
53  if (v.isEnglish) return ['french-guard: last answer looks English']
54  if (v.unaccented.length >= MIN_UNACCENTED) {
55    const sample = [...new Set(v.unaccented)].slice(0, 4).join(', ')
56
57    return [`french-guard: ${v.unaccented.length} unaccented words (${sample})`]
58  }
59
60  return []
61}
62
types/index.d.ts 8 lines
1export type GuardState = { isEnabled: boolean; last: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'french-guard': { guard: GuardState }
6  }
7}
8