Depuis la v2.1.287 du 1er octobre 2026, Claude Code active par défaut les mods : des plugins dont le code JavaScript ou TypeScript s'exécute dans le process de Claude Code. Un mod s'enregistre sur des événements (appel d'outil, prompt soumis, requête au modèle, dessin de l'interface) et peut chaque fois observer, réécrire ou répondre à la place de l'agent. Pour une équipe, c'est ce qui manquait : imposer une règle au lieu de l'écrire dans un CLAUDE.md en espérant qu'elle soit suivie.
Le même mécanisme a un revers, et la doc officielle l'écrit sans détour : « A mod is code that runs with your permissions » et « Mods aren't sandboxed ». Un mod installé voit chaque prompt et chaque appel d'outil, lit tes variables d'environnement et peut lancer des programmes en ton nom. Cet article pose ce que les mods permettent, comment en écrire un, le modèle de menace qui en découle et la gouvernance qui permet de garder l'un sans subir l'autre.
Qu'est-ce qu'un mod Claude Code, concrètement ?
Un mod Claude Code est un plugin qui embarque un module JavaScript ou TypeScript, chargé sans build ni bundler, dont les fonctions s'exécutent à l'intérieur de Claude Code sur des événements de session. Il peut dessiner un panneau ou une bande au-dessus du prompt, intercepter un appel d'outil et poser une question avant de le laisser passer, ajouter une commande /xxx exécutée sans tour de Claude, rediriger une requête vers un autre modèle ou réécrire le prompt système. Certaines fonctions natives sont déjà des mods : /diff, le chargement d'AGENTS.md, la télémétrie.
Les hooks d'un mod tournent partout où le plugin est chargé : terminal, Desktop, extension VS Code, claude -p, Agent SDK. Le dessin d'interface, lui, n'apparaît que dans le terminal et l'onglet Code de Desktop. L'API est jeune et a bougé à chaque version depuis : $.ui.selection() en v2.1.288, le cache de prompt dans $.model.complete en v2.1.292, $.ui.notify en v2.1.295. Si tu écris un mod aujourd'hui, prévois de le retoucher.
Mod, hook, skill ou serveur MCP : lequel choisir ?
Le plugin est le conteneur de distribution : il peut embarquer des skills, des agents, des serveurs MCP, des hooks de settings et un mod. Les autres notions sont des mécanismes, du moins puissant au plus puissant :
- Skill : un fichier d'instructions que Claude lit à la demande. Il change ce que Claude sait, il n'exécute rien. Les bases sont dans notre article sur les commandes et les skills.
- Serveur MCP : un processus externe, dans n'importe quel langage, qui donne des outils à Claude. À choisir quand l'agent doit joindre un système externe, comme décrit dans connecter Claude Code à tes outils internes.
- Settings hook : une commande shell, une requête HTTP ou un prompt déclenché sur un événement. Il bloque, autorise, modifie des arguments ou ajoute du contexte, mais ne dessine rien.
- Mod : du code dans le process de Claude Code, qui modifie appels d'outils, prompts, commandes, tours et interface, et peut garder un état entre les événements.
Règle de décision à défendre en revue de code : prends le mécanisme le moins puissant qui suffit. Une règle fixe (« refuse npm publish ») se code en règle de permission, sans code. Un script existant qui bloque ou journalise devient un settings hook. Le mod se justifie quand la décision dépend d'un état vivant (branche courante, valeur notée par un autre hook, réponse de l'utilisateur) ou demande de l'interface.
Comment est construit un mod minimal ?
Trois fichiers suffisent, d'après la page de création. Le manifest .claude-plugin/plugin.json est celui d'un plugin classique. C'est la clé modules du fichier hooks/hooks.json qui fait du plugin un mod :
first-mod/
├── .claude-plugin/
│ └── plugin.json # manifest de plugin classique
└── hooks/
├── hooks.json # { "modules": ["./register.js"] }
└── register.js # exporte register(on)// hooks/register.js : exemple officiel, compteur d'appels d'outils
let calls = 0
export function register(on) {
on('session.start', async ($, e, next) => {
await $.command.register({ name: 'tally', description: 'Show how many tool calls Claude has made' })
return next(e)
})
// Observer : on compte, puis on laisse l'outil s'exécuter
on('tool.call', async ($, e, next) => {
calls += 1
$.ui.invalidate('ui.render')
return next(e)
})
// Répondre : pas de next(), la commande /tally est servie par le mod
on('command.run', { command: 'tally' }, async () => {
return { text: 'Claude has made ' + calls + ' tool calls since this mod loaded' }
})
}Chaque hook reçoit trois arguments. $ est l'API, organisée en espaces de noms ($.ui, $.fs, $.process, $.http, $.model, $.session). e est l'événement, gelé en profondeur : pour le modifier, tu passes une copie. next appelle le maillon suivant d'une chaîne dont le dernier est le comportement natif. D'où trois verbes : observer (return next(e)), réécrire (next({ ...e, champ })), répondre (retourner un objet sans appeler next, par exemple { deny: 'raison' }).
# Charger le mod pour une session, avec rechargement à chaud
claude --plugin-dir ./first-mod
# Analyse statique : ce que Claude Code lit réellement dans le mod
claude plugin validate ./first-mod
# ❯ ./register.js hooks: session.start, tool.call, command.run{command=tally}
# ❯ ./register.js calls: $.command.register, $.ui.invalidateL'analyse statique impose ses règles : chaque appel d'API s'écrit en entier ($.store.get(...), pas de déstructuration de $), le nom d'événement est une chaîne littérale, pas d'import() dynamique ni de require, et seuls les imports relatifs internes au plugin sont permis. Claude Code génère aussi des fichiers .d.ts exacts pour ta version dans .claude-plugin/types/ : la doc recommande de leur faire confiance plutôt qu'aux pages si les deux divergent. Enfin, claude plugin test exécute des tests de mod sans session, sans connexion et sans réseau.
Un garde-fou Symfony : refuser doctrine:schema:update --force
Première question : une règle deny suffit-elle ? Souvent oui, et c'est le choix à privilégier. Le mod se justifie quand tu veux que le refus instruise Claude sur la procédure de l'équipe, ici les migrations Doctrine. Le message de deny est lu par Claude comme le résultat de l'outil : écris-le comme une consigne actionnable, pas comme une erreur.
// hooks/register.js : rappel pour Claude, PAS une barrière de sécurité
const FORBIDDEN = ['doctrine:schema:update', 'doctrine:schema:drop', 'doctrine:database:drop']
const REASON =
'Interdit ici : pas de schema:update, schema:drop ni database:drop. ' +
'Génère une migration avec bin/console make:migration, relis-la, puis doctrine:migrations:migrate.'
function isForbidden(command) {
return command.includes('--force') && FORBIDDEN.some((name) => command.includes(name))
}
export function register(on) {
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
if (isForbidden(e.command)) {
return { deny: REASON } // pas de next() : la commande ne s'exécute pas
}
return next(e)
}).catch(async ($, e, next) => {
// Fail-closed : si le hook jette ou dépasse son budget, on refuse
if (next.called) return next(e)
return { deny: 'Le garde-fou Doctrine a échoué, commande non exécutée : ' + next.error.kind }
})
}- Un test sur du texte se contourne :
sh -c "$CMD", variables, et surtout les abréviations que la console Symfony accepte (d:s:u --forcepasse ici). La doc qualifie elle-même ce type de hook de rappel pour Claude. La vraie barrière reste côté infrastructure : identifiants de production hors de portée du poste, droits DDL retirés, branche protégée. - Les hooks
tool.callne voient que les appels de Claude, pas ce qu'unMakefileou un script lance ensuite en sous-processus. - Sans
.catch, un hook qui échoue est sauté : il échoue ouvert.claude plugin validatele signale pargating hook without .catch. - L'événement
tool.checkpermet une décision contextuelle (refuser une migration surmain, par exemple), mais untool.checkqui répondallowpeut approuver ce qu'une règleaskaurait demandé. Un mod mal écrit affaiblit ta sécurité au lieu de la renforcer.
Votre équipe utilise Claude Code ?
Formation intra sur vos projets réels : de la demi-journée à 2 jours, 6 devs max.
Quel est le modèle de menace des mods ?
Un mod est du code tiers qui tourne avec tes droits, hors sandbox. La sandbox de Claude Code isole les commandes Bash de l'agent ; un processus lancé par un mod via $.process.run tourne en dehors, comme expliqué dans l'article sur l'auto mode et la sandbox. D'après la page d'administration, un mod chargé peut :
- Agir sur la machine en ton nom : lire et écrire partout où ton compte le peut, lancer des programmes, faire des requêtes réseau.
- Lire tes secrets : variables d'environnement (
$.env.get) et fichiers de settings, donc tes clés d'API. - Voir et modifier ta session : réécrire un prompt ou un appel d'outil, soumettre un prompt comme si tu l'avais tapé, réécrire le transcript avant son stockage.
- Approuver à ta place : en auto mode, un appel approuvé par un mod via
tool.checks'exécute sans passer par le classifieur. - Dépenser ton usage :
$.model.completeconsomme ton plan ou ta clé.
Deux nuances contredisent des raccourcis fréquents. D'abord, les règles deny ne protègent pas contre $.fs et $.process : avec Read(./.env) interdit, un mod lit quand même .env, car la règle ne couvre que les outils de Claude. Ensuite, la seule chose qu'un mod ne peut pas redessiner est le dialogue de permission : tu vois toujours ce que tu approuves, mais un mod peut approuver à ta place. Les vecteurs réalistes sont ceux de toute chaîne d'approvisionnement, décrits dans notre article sur la prompt injection et la supply chain : un plugin de marketplace tiers qui exfiltre une clé via $.http.fetch, une mise à jour malveillante d'un plugin légitime, un mod qui injecte des instructions dans le prompt système.
Un mod peut aussi arriver sans marketplace : Claude peut en écrire un pendant une session, activé après approbation du rechargement. Côté poste, --safe-mode désactive tous les mods installés pour une session, et aucun mod ne se charge avant la réponse au prompt de confiance d'un dossier non approuvé.
Comment gouverner les mods dans une équipe ?
Traite les mods comme tes dépendances de production, avec une politique posée dans les managed settings. La page d'administration décrit plusieurs postures, de la plus ouverte à la plus fermée :
- Par défaut : mods actifs, avec le garde intégré
sec-default@builtinchargé en premier dès qu'il existe des managed settings ou un compte Team ou Enterprise. Il protège ce que tu gères (hooks managés, prompt système, CLAUDE.md managé), tout le reste est permis. Avec une clé API ou un cloud tiers sans managed settings, il n'est pas chargé. - Marketplaces approuvées seulement : allowlist de marketplaces et
disableSideloadFlags: true, qui rejette--plugin-diret empêche les mods écrits par Claude de se charger. - Seulement les mods de l'organisation : option
allowManagedModsOnlydu garde, plus tes propres mods déployés. - Plus aucun hook :
disableAllHooks: true, au prix de tes propres hooks managés et de la status line.
{
"extraKnownMarketplaces": {
"acme-tools": {
"source": { "source": "directory", "path": "/opt/acme/claude-plugins" }
}
},
"enabledPlugins": { "acme-guard@acme-tools": true },
"prependPlugins": ["acme-guard@acme-tools", "sec-default@builtin"],
"pluginConfigs": {
"cc-plugin-sec-default@builtin": {
"options": { "allowManagedModsOnly": true }
}
},
"disableSideloadFlags": true
}Trois points font la différence en pratique. Un mod ne compte comme celui de l'organisation que s'il est chargé en place depuis un répertoire absolu de la machine : un plugin installé depuis GitHub ou npm est copié dans le cache et compte comme un mod utilisateur, même activé en managed. Ce répertoire doit être inscriptible uniquement par un administrateur, et déposé par ton MDM. Enfin, prependPlugins remplace la liste par défaut : si tu oublies sec-default@builtin, tu perds le garde.
Le point fort de la page d'administration est le mod de politique. Branché sur l'événement plugin.register, il reçoit avant chaque chargement la liste exacte des appels d'API qu'utilise le mod candidat, et peut le refuser :
// acme-guard/hooks/register.js : refuse les mods utilisateur qui lancent des processus
const BLOCKED_CALLS = ['process.run', 'process.spawn']
async function checkMod($, e, next) {
const blocked = e.uses.calls.filter((call) => BLOCKED_CALLS.includes(call))
if (e.tier === 'user' && blocked.length > 0) {
return { refuse: 'Acme policy: mods may not call ' + blocked.join(', ') }
}
return next(e)
}
export function register(on) {
on('plugin.register', checkMod).catch(async ($, e, next) => {
if (e.tier !== 'user') return next(e)
return { refuse: 'Acme policy check failed, so this mod was not loaded' }
})
}Chaque appel de l'API est lui-même un événement (fs.read, http.fetch, process.run) : un mod de politique peut donc observer ou refuser ce que font les autres mods, ce qu'aucune règle de permission ne permet. Limite à annoncer honnêtement : si le worker qui exécute les mods plante trois fois, Claude Code décharge tous les mods non intégrés, le tien compris. Pour une garantie dure, double toujours avec des hooks PreToolUse managés, exécutés avant tout mod et dont le blocage est final, et avec des règles de permission.
Comment relire un mod tiers avant de l'installer ?
- Cloner les sources sans installer, puis lancer
claude plugin validateet lire les ligneshooks:etcalls:. Le validateur refuse un mod dont l'usage de l'API est illisible statiquement, ce qui limite l'obfuscation. - Lire les
calls:comme des capacités :$.process.runlance des programmes hors sandbox,$.http.fetchcombiné à$.env.getrend l'exfiltration possible,$.prompt.submitparle en ton nom,$.model.completeconsomme ton plan. - Lire les
hooks:comme une portée :tool.calletprompt.submitvoient tout,tool.checkpeut approuver,session.appendréécrit le transcript. - Lire le code lui-même : domaines inconnus, constantes encodées, chemins comme
~/.sshou~/.aws. - Tester dans un conteneur jetable, avec un compte sans secrets, puis relire la sortie de
claude --debug. - Épingler la version dans ta marketplace interne et décider explicitement quand une mise à jour est reprise.
Même les mods intégrés méritent ce regard. You should know, désactivé par défaut et activable par /plugin enable cc-plugin-you-should-know@builtin, est un agent secondaire qui relit la conversation et signale ce que toi ou Claude pourriez manquer. Il n'a aucun outil, donc ne lit aucun fichier, mais chaque vérification ajoute plusieurs milliers de tokens d'instructions facturés : mesure son coût réel avec les méthodes de l'article sur le suivi de consommation avant de le généraliser.
Faut-il adopter les mods maintenant ?
Oui, à condition de décider d'abord ta posture. Commence par inventorier les plugins et mods actifs sur chaque poste : si personne ne peut répondre, tu n'as pas de gouvernance. Choisis ensuite entre des mods fermés (allowManagedModsOnly et disableSideloadFlags) et des mods ouverts encadrés par un mod de politique qui refuse process.*. Écris deux ou trois garde-fous à fort rendement pour ta stack, avec .catch en fail-closed, et mets claude plugin validate en CI sur ta marketplace interne. Un mod bien cadré devient un vrai levier d'outillage ; un mod installé au hasard est du code tiers qui lit ta session avec tes droits.
Une nouveauté de cette ampleur se décide une fois, pour toute l'équipe, plutôt que poste par poste. Prochain article de la série : RTK 1.0 en production, un autre outil qui s'insère entre toi et l'agent, avec les mêmes questions de confiance.

Écrit par
Flavien Métivier
Expert PHP/Symfony indépendant depuis 2014, ancien CTO de Kartable et d'Extrabat. Plus de 80 projets menés, de l'audit à la migration.
Mon parcours