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

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.
| Mod | Ce qu'il fait |
|---|---|
| 🛡️ garde-du-corps | Bloque 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. |
| 🚀 cockpit | Un 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 tonsettings.json.
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
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
claude plugin validate garde-du-corps && claude plugin test garde-du-corps
claude plugin validate cockpit && claude plugin test cockpit
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.
Licence MIT.
hooks/register.ts 48 lines1import { 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}
48hooks/rules.ts 77 lines1// 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}
77types/index.d.ts 8 lines1export type BlockedCount = number
2
3declare module 'claude-code' {
4 interface PluginState {
5 'garde-du-corps': { blocked: BlockedCount }
6 }
7}
8