SLOPSHOPPER

garde-du-corps

Bloque l'accès aux fichiers .env et les commandes Bash destructrices, avec toast et compteur dans la status line.

newguardtoaststatus
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · garde-du-corps
› fix the failing auth test and add an audit log call ╭───────────────────────────────────╮ │ garde-du-corps │ ⏺ Read(src/auth.ts) │ 🛡️ Garde-du-corps : rm -rf │ ⎿ Read 6 lines │ bloqué │ ⏺ Update(src/auth.ts) ╰───────────────────────────────────╯ ⎿ Added 2 lines, removed 1 line ╭────────────────────────────────────────────╮ ⏺ Bash(rm -rf build && git push --force origin main) │ garde-du-corps │ ⎿ Denied by garde-du-corps: Bloqué par le garde-du-corps (rm -rf). Ce│ 🛡️ Garde-du-corps : accès au fichier .env │ │ via Bash bloqué │ ● Done. refresh now rejects expired claims and logs an audit event. ╰────────────────────────────────────────────╯ ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ garde-du-corps: 🛡️ 2 actions bloquées
README

Claude Code Mods 🇫🇷

Deux Claude Mods construits en live dans la vidéo YouTube de Claude Code France : Claude Code les a écrits lui-même, à partir d'une seule phrase chacun.

ModCe qu'il fait
🛡️ garde-du-corpsBloque la lecture et la modification des fichiers .env (les modèles .env.example, .env.sample… restent accessibles) et les commandes Bash destructrices : rm -rf, git push --force (et --force-with-lease), git reset --hard, DROP TABLE. Chaque blocage affiche un toast, et la status line compte les actions bloquées.
🚀 cockpitUn panneau live à côté de la conversation : modèle, coût, remplissage du contexte, nombre de tours, d'appels d'outils et de tokens, outils les plus utilisés, durée de chaque tour et derniers fichiers modifiés. S'ouvre avec /cockpit.

Prérequis : Claude Code 2.1.287 ou plus récent (les mods sont activés par défaut). Pour que le cockpit s'affiche en barre latérale, mets "tui": "fullscreen" dans ton settings.json.

Installation

Via la marketplace (le plus simple)

Dans Claude Code :

/plugin marketplace add Para-FR/claude-code-mods-fr
/plugin install garde-du-corps@claude-code-mods-fr
/plugin install cockpit@claude-code-mods-fr

En clonant le repo

git clone https://github.com/Para-FR/claude-code-mods-fr.git ~/claude-code-mods-fr
claude --plugin-dir ~/claude-code-mods-fr/garde-du-corps --plugin-dir ~/claude-code-mods-fr/cockpit

Vérifier et tester

claude plugin validate garde-du-corps && claude plugin test garde-du-corps
claude plugin validate cockpit && claude plugin test cockpit

Les prompts qui ont créé ces mods

garde-du-corps

Crée-moi un mod Claude Code "garde-du-corps" : il bloque toute lecture ou modification des fichiers .env, ainsi que les commandes Bash dangereuses (rm -rf, git push --force, git reset --hard, DROP TABLE). Quand il bloque quelque chose, affiche un toast, et garde dans la status line un compteur des actions bloquées.

cockpit

Crée un mod "cockpit" : un panneau live à côté de la conversation qui affiche, pour cette session, le nombre de tours, d'appels d'outils et de tokens, un histogramme des outils les plus utilisés, la durée de chaque tour en barres, et la liste des derniers fichiers modifiés. Ajoute une commande /cockpit pour l'ouvrir.

⚠️ À savoir

  • Lis le code d'un mod avant de l'installer : un mod tourne dans Claude Code et voit tes prompts, tes outils et tes fichiers. Ceux-ci sont courts : lis-les 🙂
  • Early access : l'API des mods peut changer d'une version de Claude Code à l'autre.
  • Garde-fou, pas coffre-fort : le garde du corps bloque les cas évidents, pas toutes les variantes possibles. Ne compte pas uniquement sur lui pour protéger de vrais secrets de production.

Liens

Licence MIT.

Source 3 files
hooks/register.ts 48 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, ToolCallInput } from 'claude-code'
3
4import { checkCommand, checkPath } from './rules'
5
6const blocked = atom({ plugin: 'garde-du-corps', key: 'blocked' } as const, 0)
7
8const showCount = ($: EngineInterface, count: number) =>
9  $.ui.status(`🛡️ ${count} action${count > 1 ? 's' : ''} bloquée${count > 1 ? 's' : ''}`)
10
11const reasonFor = (e: ToolCallInput): string | undefined => {
12  switch (e.tool) {
13    case 'Read':
14    case 'Edit':
15    case 'Write':
16      return checkPath(e.file_path)
17    case 'NotebookEdit':
18      return checkPath(e.notebook_path)
19    case 'Bash':
20      return checkCommand(e.command)
21    default:
22      return undefined
23  }
24}
25
26export const register: Register = on => {
27  on('session.start', async ($, e, next) => {
28    showCount($, await read($, blocked))
29
30    return next(e)
31  })
32
33  on('tool.call', async ($, e, next) => {
34    const reason = reasonFor(e)
35
36    if (reason === undefined) {
37      return next(e)
38    }
39
40    showCount($, await update($, blocked, n => n + 1))
41    $.ui.toast(`🛡️ Garde-du-corps : ${reason} bloqué`)
42
43    return {
44      deny: `Bloqué par le garde-du-corps (${reason}). Cette action est interdite dans cette session : ne cherche pas à la contourner, demande à l'utilisateur de la faire lui-même si elle est vraiment nécessaire.`,
45    }
46  })
47}
48
hooks/rules.ts 77 lines
1// Règles pures du garde-du-corps : chaque fonction renvoie la raison du
2// blocage, ou undefined quand l'action est autorisée.
3
4// Modèles de fichiers sans secret, laissés accessibles.
5const ALLOWED_SUFFIXES = new Set(['.example', '.sample', '.template', '.dist'])
6
7const isProtectedEnvName = (name: string): boolean => {
8  const match = /^\.env(\.[\w.-]+)?$/.exec(name)
9
10  return match !== null && !ALLOWED_SUFFIXES.has(match[1] ?? '')
11}
12
13export const checkPath = (path: string): string | undefined => {
14  const name = path.split(/[\\/]/).pop() ?? ''
15
16  return isProtectedEnvName(name) ? `accès au fichier ${name}` : undefined
17}
18
19// `.env` ou `.env.xxx` cité comme mot dans une commande (cat .env,
20// source ./config/.env.local, cp .env.production /tmp…).
21const ENV_IN_COMMAND = /(?:^|[\s'"`=<>|;&(:\/])(\.env(?:\.[\w.-]+)?)(?=$|[\s'"`|;&)<>])/g
22
23const splitSegments = (command: string): string[] =>
24  command.split(/&&|\|\||[;|&\n]/).map(s => s.trim())
25
26const hasRmRecursiveForce = (segment: string): boolean => {
27  const tokens = segment.split(/\s+/)
28  const at = tokens.findIndex(t => t === 'rm' || t.endsWith('/rm'))
29
30  if (at === -1) {
31    return false
32  }
33
34  const flags = tokens.slice(at + 1).filter(t => t.startsWith('-'))
35  const short = flags.filter(f => !f.startsWith('--')).join('')
36  const isRecursive = /[rR]/.test(short) || flags.includes('--recursive')
37  const isForced = short.includes('f') || flags.includes('--force')
38
39  return isRecursive && isForced
40}
41
42const hasForcePush = (segment: string): boolean =>
43  /\bgit\b.*\bpush\b/.test(segment) &&
44  segment
45    .split(/\s+/)
46    .some(t => /^--force(-with-lease|-if-includes)?(=.*)?$/.test(t) || /^-[a-zA-Z]*f[a-zA-Z]*$/.test(t))
47
48const hasHardReset = (segment: string): boolean => /\bgit\b.*\breset\b.*(^|\s)--hard\b/.test(segment)
49
50export const checkCommand = (command: string): string | undefined => {
51  if (/\bdrop\s+table\b/i.test(command)) {
52    return 'DROP TABLE'
53  }
54
55  const segments = splitSegments(command)
56
57  if (segments.some(hasRmRecursiveForce)) {
58    return 'rm -rf'
59  }
60
61  if (segments.some(hasForcePush)) {
62    return 'git push --force'
63  }
64
65  if (segments.some(hasHardReset)) {
66    return 'git reset --hard'
67  }
68
69  for (const [, name = ''] of command.matchAll(ENV_IN_COMMAND)) {
70    if (isProtectedEnvName(name)) {
71      return `accès au fichier ${name} via Bash`
72    }
73  }
74
75  return undefined
76}
77
types/index.d.ts 8 lines
1export type BlockedCount = number
2
3declare module 'claude-code' {
4  interface PluginState {
5    'garde-du-corps': { blocked: BlockedCount }
6  }
7}
8