Depuis le 1er octobre 2026, tu peux écrire un mod : un plugin dont le code JavaScript ou TypeScript tourne à l'intérieur de Claude Code.
Le principe
Jusqu'ici, tu changeais Claude Code depuis l'extérieur, avec un script shell branché sur un hook ou un skill que Claude lit. Les serveurs MCP, eux, lui ajoutent des outils. Un mod tourne à l'intérieur.
Un plugin qui contient du code JavaScript ou TypeScript que Claude Code appelle dans son propre processus, à chaque événement qui l'intéresse : un appel d'outil, un prompt envoyé, une partie de l'interface à dessiner. Le code peut regarder l'événement, le modifier ou y répondre à la place de Claude Code. Il tourne avec tes droits, sans sandbox.
Les mods sont arrivés avec Claude Code v2.1.287 dans le terminal (l'app desktop les fait tourner depuis sa v2.1.286). Ils sont activés par défaut. Ce chapitre est écrit sur la v2.1.296 du 9 octobre 2026, et l'API est en early access : elle bouge d'une version à l'autre.
Ce que seul un mod fait :
- un panneau à côté de la conversation ou un bandeau au-dessus du prompt, avec des boutons et des champs de saisie ;
- sa propre version de ce que Claude Code affiche déjà, comme la ligne d'un appel d'outil ou le spinner ;
- retenir un appel d'outil le temps de te poser une question, ou y répondre sans lancer l'outil ;
- une commande
/quelquechosequi exécute ton code tout de suite, sans tour de Claude, même pendant que Claude travaille.
Une limite : la fenêtre de permission. Un mod peut approuver ou refuser un appel avant qu'elle apparaisse, mais il ne peut pas modifier ce qu'elle affiche.
Où ça tourne
| Où tu lances Claude Code | Les hooks du mod tournent | Ce qu'il dessine s'affiche |
|---|---|---|
claude dans un terminal (y compris celui de ton éditeur) | Oui | Oui |
| Onglet Code de l'app desktop | Oui | Oui, sauf les éléments réservés au terminal |
| Session WSL dans l'app desktop | Non | Non |
| Panneau de chat de l'extension VS Code | Oui | Non |
claude -p et l'Agent SDK | Oui | Non |
| Remote Control depuis claude.ai ou le mobile | Oui, sur ta machine | Dans le terminal de ta machine |
| Session cloud | Oui, si le plugin y est | Non |
Mod, hook, skill, MCP, plugin : qui fait quoi
Un plugin est l'emballage : il s'installe en une commande et peut contenir des skills, des commandes, des subagents, des hooks shell, des serveurs MCP et, depuis octobre, un mod.
Le hook que tu connais déjà (chapitre
9) : une commande shell, une
requête HTTP ou un prompt déclaré dans un fichier settings.json, que Claude
Code lance sur un événement comme PreToolUse. La doc l'appelle désormais «
settings hook ». Dans ce chapitre, « hook » tout court désigne une fonction
d'un mod.
| Mod | Hook de réglages | Skill | Serveur MCP | |
|---|---|---|---|---|
| Ce que c'est | Des fonctions que Claude Code appelle dans son propre processus | Une commande shell, une requête HTTP ou un prompt sur un événement | Un fichier SKILL.md d'instructions | Un processus externe qui donne des outils |
| Ce qu'il peut changer | Appels d'outils, prompts, commandes, tours, ce que l'interface dessine | Laisser passer ou bloquer, modifier les arguments, ajouter du contexte | Ce que Claude sait et fait | Les outils dont Claude dispose |
| Dessine dans l'interface | Oui | Non | Non | Non |
| Ce que tu écris | JavaScript ou TypeScript | Un script et une entrée dans settings.json | Du markdown | Un serveur, dans le langage que tu veux |
| Choisis-le quand | Tu veux un panneau, un bandeau, une commande à toi, ou réécrire un événement | Tu veux bloquer, autoriser ou journaliser avec un script existant | Tu recolles les mêmes consignes dans le chat | Claude doit atteindre un système externe |
Les hooks de réglages ne sont pas dépréciés : ils tournent à côté des mods, et un script qui marche n'a aucune raison d'être réécrit.
Essayer un mod sans rien écrire
Tu en utilises déjà. Plusieurs fonctions de Claude Code sont des mods intégrés, rangés sous Built-in dans /plugin, onglet Installed. cc-plugin-diff, par exemple, c'est le panneau de /diff. Les autres chargent AGENTS.md comme instructions de projet (cc-plugin-agents-md), donnent à Claude le skill plugin-authoring (cc-plugin-plugin-authoring), protègent ce que ton organisation gère des mods que tu installes (cc-plugin-sec-default) et envoient les données d'usage de Claude Code (cc-plugin-telemetry).
Le plus parlant à essayer est cc-plugin-you-should-know, désactivé par défaut. Sur une tâche longue, un agent annexe relit ce que fait Claude et te note au-dessus du prompt ce que toi ou Claude risquez de rater. D'après le changelog, il marche dans les sessions directes chez Anthropic, télémétrie activée.
/plugin enable cc-plugin-you-should-know@builtin
Pour voir à quoi ressemble un vrai mod, le code de diff, agents-md, sec-default et telemetry est public, tests compris, dans le dossier mods du repo claude-code. Anthropic publie aussi des mods d'exemple, sans support, dans anthropics/claude-code-playground : token-weather (une prévision de ta fenêtre de contexte au-dessus du prompt), blast-radius (retient une commande risquée comme rm -rf ou un force push et te montre ce qu'elle changerait), replay-theater (une commande /replay qui rejoue les modifs de fichiers du dernier tour). Pour en tester un sans l'installer :
git clone https://github.com/anthropics/claude-code-playground
claude plugin validate claude-code-playground/claude-code/mods/blast-radius
claude --plugin-dir claude-code-playground/claude-code/mods/blast-radius
Dans la session, /plugin affiche sous ses onglets une ligne comme 1 mod active · blast-radius. Les mods intégrés n'y figurent pas.
Écrire le tien avec /plugin-authoring
Pas besoin de connaître l'API pour commencer : le skill intégré plugin-authoring sait où écrire le mod, ce que ta version propose et comment le charger. Claude le charge seul quand tu demandes un mod, ou tu le lances toi-même.
- Décris le mod.
/plugin-authoringpuis ta demande, ou directement « fais-moi un mod qui affiche la branche git au-dessus du prompt ». Claude l'écrit dans~/.claude/dev-mods/<id de session>/<nom du mod>/. C'est un chemin protégé : en mode manuel ou accept edits, Claude Code te demande avant chaque fichier. - Active le rechargement. Au premier fichier, Claude Code propose le rechargement à chaud. « Enable for this session » charge le mod à la fin du tour, puis le recharge à chaque tour qui le modifie. Avec « Not now », les fichiers restent là et rien ne se charge.
- Vérifie et itère. Le mod apparaît dans
/plugin, onglet Installed, où tu peux le couper. Dis à Claude ce qui ne va pas : il corrige, lanceclaude plugin validate, et tu testes dès la fin du tour. - Garde-le. Un mod écrit ainsi ne vit que dans cette session, et son dossier est effacé avec les vieilles sessions (
cleanupPeriodDays). Copie-le ailleurs, par exemple~/mods/git-branch, puis charge-le avecclaude --plugin-dir ~/mods/git-branch.
Le mod ne se charge pas dans une session claude -p ou en mode dontAsk (personne pour valider), dans un dossier que tu n'as pas approuvé, ni avec --safe-mode, --bare ou disableAllHooks.
Pour modifier un mod existant, lance la session avec claude --plugin-dir ./mon-mod et demande le changement : il se recharge dans la même session. Claude Code dépose aussi les déclarations TypeScript de ta version dans .claude-plugin/types/ du mod. Quand elles contredisent la doc, ce sont elles qui ont raison.
claude plugin init ne crée pas un mod. La commande génère un plugin
classique (hooks shell, skills) dans ~/.claude/skills/. Un mod, c'est les
trois fichiers de l'exemple qui suit, écrits à la main ou par Claude.
Exemple 1 : un garde-fou sur rm -rf et les force push
Le besoin : Claude ne lance ni rm -rf, ni git push --force vers main, sans que tu aies dit oui. Les autres commandes passent sans question.
Trois fichiers :
garde-fou/
├── .claude-plugin/
│ └── plugin.json # le manifeste, comme pour tout plugin
└── hooks/
├── hooks.json # pointe vers ton code
└── register.ts # ton code : le hooks module
.claude-plugin/plugin.json :
{
"name": "garde-fou",
"version": "0.1.0",
"description": "Demande confirmation avant un rm -rf ou un git push --force vers main",
"author": { "name": "Ton nom" }
}
hooks/hooks.json :
{
"description": "Hooks module of garde-fou",
"modules": ["./register.ts"]
}
hooks/register.ts (les commentaires suffisent pour suivre, l'anatomie juste après détaille register, on et next) :
import type { Register } from "claude-code";
// rm with both -r and -f, in any order (rm -rf, rm -fr, rm -Rf)
const RM_RF = /\brm\s+-(?=[a-z]*r)(?=[a-z]*f)[a-z]+/i;
// git push with --force or -f, aimed at main
const FORCE_MAIN = /\bgit\s+push\b(?=.*(--force\b|\s-f\b))(?=.*\bmain\b)/;
export const register: Register = (on) => {
on("tool.call", { tool: "Bash" }, async ($, e, next) => {
// Every other command goes on to the usual permission check
if (!RM_RF.test(e.command) && !FORCE_MAIN.test(e.command)) return next(e);
// Safe default: a question nobody answers refuses the command
let answer = "Refuser";
try {
// The tool call waits here, outside the hook's time limit
answer = await $.ui.ask(`On lance \`${e.command}\` ?`, [
"Lancer",
"Refuser",
]);
} catch {
// Dismissed, or a claude -p run with nobody to ask
}
if (answer === "Lancer") return next(e);
// No call to next: the command never runs, Claude reads this text
return {
deny: "L'utilisateur a refusé cette commande. Demande-lui avant d'essayer autre chose.",
};
}).catch(($, e, next) =>
// Fail closed: if the guard crashed before deciding, nothing runs
next.called
? next(e)
: { deny: "Le garde-fou a planté : commande non lancée." },
);
};
Quand Claude tente rm -rf build, l'appel s'arrête et la question s'affiche là où Claude te pose ses questions, avec la commande dedans. Lancer envoie la commande au contrôle de permission habituel, qui s'applique quand même. Si tu refuses, tapes autre chose ou fermes la question, la commande ne tourne pas et Claude lit le texte du deny comme résultat de l'outil (d'où la consigne écrite pour lui). En claude -p, personne ne peut répondre : $.ui.ask échoue et la commande est refusée.
L'attente se fait dans $.ui.ask, qui ne compte pas dans les 10 secondes allouées au hook. Une attente dans ta propre promesse compterait, et un hook hors délai est sauté : la commande passerait.
Vérification, sans rien installer :
$ claude plugin validate ./garde-fou
❯ ./register.ts hooks: tool.call{tool=Bash}
❯ ./register.ts gating hook with .catch: tool.call{tool=Bash}
❯ ./register.ts calls: $.ui.ask
✔ Validation passed
La ligne gating hook with .catch confirme que le hook peut refuser une action et qu'il a sa roue de secours (sans le .catch, elle dirait without .catch).
Ce garde-fou n'arrête pas tout. Le mod lit le texte de la commande : rm -r -f, un alias ou un script qui fait le rm à sa place passent à travers.
Pour protéger main pour de bon, protège la branche chez ton hébergeur Git. La
doc le dit elle-même de son exemple de garde sur git push.
Anatomie d'un mod
Les fichiers
Le manifeste du garde-fou n'a rien de spécial. Ce qui fait d'un plugin un mod, c'est la clé modules de hooks/hooks.json : le chemin d'un seul fichier, relatif à hooks.json.
Le fichier que modules désigne. Il exporte une fonction register(on, options) que Claude Code appelle au chargement. options contient les
réglages que l'utilisateur a saisis (on y revient avec userConfig). Le
fichier est un module ES en .js, .ts, .tsx, .mjs ou cousin : pas de
require, pas de import() dynamique, pas de build à faire.
Le code tourne isolé, sans Node ni DOM : pas de fs, de fetch global ni de setTimeout. Les API web standard (URL, TextEncoder, AbortController, crypto.subtle) sont là, et tout le reste passe par un objet que Claude Code te tend : $.
register, on, et la forme d'un hook
Dans register, chaque appel à on branche une fonction sur un événement :
on("tool.call", { tool: "Bash" }, async ($, e, next) => next(e));
Le deuxième argument, facultatif, est le matcher : un filtre sur les champs de l'événement. Une valeur ({ tool: 'Bash' }), une liste ({ tool: ['Edit', 'Write'] }) ou une regex ({ tool: /^mcp__github__/ }). Sans matcher, ta fonction voit tous les événements de ce nom.
Chaque hook reçoit trois arguments :
$, l'interface du moteur : tout ce que ton mod peut faire hors de son propre code (afficher, appeler un modèle, lire un fichier, lancer une commande) passe par là ;e, l'événement, en données simples et gelées : le nom de l'outil et ses arguments, le texte du prompt... Pour le changer, tu passes une copie ànext;next, la suite :next(e)passe l'événement aux autres mods, puis au comportement normal de Claude Code, et renvoie le résultat.
Ce que ton hook renvoie détermine l'effet :
| Ton hook renvoie | Effet |
|---|---|
next(e) | Observer : l'événement continue tel quel |
next({ ...e, text: e.text.trim() }) | Réécrire : la suite ne voit que ta version |
Un objet sans appeler next, comme { deny } | Répondre : ni les autres mods ni Claude Code n'interviennent |
Le résultat de await next(e), après ton code | Agir après coup, une fois l'outil passé, et éventuellement retoucher le résultat |
La chaîne
Quand plusieurs mods écoutent le même événement, leurs hooks forment une chaîne, dans cet ordre :
- le garde intégré
sec-defaultet les mods de ton organisation placés en tête ; - les mods que tu as installés ;
- les mods de l'organisation placés en queue ;
- les autres mods intégrés à Claude Code.
Pour un appel d'outil, les hooks PreToolUse de tes réglages s'exécutent au bout de la chaîne, dans le comportement normal de Claude Code : un mod qui répond sans appeler next les court-circuite. Ceux des réglages gérés par une organisation passent avant tous les mods, et leur blocage est définitif.
Quand un hook plante
Un hook qui lève une erreur, dépasse son temps (10 secondes de calcul propre, hors attente de next ou d'un appel à $) ou renvoie une réponse mal formée est sauté, et la chaîne continue sans lui. Pour un garde-fou, c'est le pire cas : la commande dangereuse passerait. D'où le .catch à la fin du garde-fou, qui répond à la place du hook planté. next.called dit si le hook avait déjà passé la main avant de planter : si oui, on garde ce résultat, sinon on refuse.
Les règles de l'analyse statique
Claude Code lit ton code sans l'exécuter pour savoir ce qu'il fait, et refuse ce qu'il ne sait pas lire. D'où quelques règles :
- écris chaque appel en entier :
$.fs.read(...), jamaisconst fs = $.fsni de déstructuration de$; - donne le nom de l'événement en texte littéral (
'tool.call'), pas dans une variable ni une boucle ; - tu peux passer
$à une fonction déclarée au niveau du fichier, pas à une fonction définie dans le hook ; - importe seulement des fichiers du plugin, par chemin relatif. Seul import nu autorisé :
claude-code, pour les types et quelques helpers.
Ce qu'un mod peut faire
Cette partie sert de référence. Survole-la maintenant, tu y reviendras en écrivant ton mod.
Les événements
| Famille | Événements | Exemple d'usage |
|---|---|---|
| Outils | tool.call, tool.check, tool.describe | Bloquer un git push sur main, répondre à la place d'un outil |
| Prompts | prompt.submit, prompt.section, prompt.context, prompt.mention, skill.prompt | Ajouter la branche courante quand tu parles de PR |
| Commandes et réglages | command.run, command.describe, config.set, config.describe | Servir ta propre commande, refuser un changement de /config |
| Tours | turn.start, turn.step, turn.complete | Lire les tokens de chaque requête, envoyer une requête à un autre modèle |
| Session | session.start, session.end, session.compact, session.measure, session.append... | Enregistrer tes commandes au démarrage, suivre le contexte |
| Subagents | agent.offer, agent.spawn | Choisir le modèle d'un subagent, en masquer un |
| Interface | ui.render, ui.press, ui.input, ui.select, ui.close... | Dessiner un panneau, réagir à un bouton |
| Autres mods | plugin.register, engine.create | Refuser un mod au chargement (politique d'entreprise) |
| Hooks de réglages | classic.Stop, classic.PostToolUse... (classic.* pour tous) | Réagir aux mêmes événements qu'un hook shell, avec le même JSON |
Chaque méthode de $ est elle-même un événement (fs.read, model.complete...). Un mod placé plus tôt dans la chaîne peut donc observer, réécrire ou refuser ce que font les mods suivants. C'est comme ça qu'une organisation encadre les mods de ses équipes.
tool.call et tool.check ne font pas la même chose. tool.call intercepte l'appel avant le contrôle de permission. tool.check arrive après les règles et les hooks de réglages : next(e) y renvoie leur décision (allow, ask ou deny) et ton hook peut la remplacer.
Ce que l'objet $ expose
| Espace | Ce qu'il permet |
|---|---|
$.ui | Ouvrir un panneau (open), redessiner (invalidate), afficher une notification (toast), une ligne sous le prompt (status), une ligne grisée dans la conversation (log), poser une question (ask), copier, notifier le système |
$.command | Enregistrer une commande, en lancer une, lister les commandes |
$.tool | Déclarer un outil que Claude peut appeler, en appeler un |
$.model | complete (un prompt seul, au modèle de ton choix), fork (une question posée sur la conversation en cours), classify |
$.prompt | Soumettre un prompt (ça lance un tour), remplir le champ de saisie, lire ce que tu tapes |
$.session | La conversation (messages), le dossier, le modèle, la conso (usage), compacter, envoyer un message à une autre session |
$.agent | Déclarer un type de subagent, en lancer un, lister ceux qui tournent |
$.fs, $.process, $.http | Lire et écrire des fichiers, lancer une commande (sans shell), faire une requête réseau |
$.store, $.state | Garder des valeurs entre sessions (4 Mio de JSON au total), ou un état réactif qui redessine l'interface quand il change |
$.clock | L'heure, et des minuteries (every, after) qui remplacent setInterval et setTimeout |
$.env, $.settings, $.config | Lire ou poser une variable d'environnement, lire les réglages, les lignes de /config |
$.mcp | Appeler l'outil d'un serveur MCP connecté |
Pour dessiner, un hook ui.render vise un emplacement (Pane pour un panneau, AbovePrompt pour le bandeau, Spinner, ToolUse, UserMessage...) et renvoie un arbre d'éléments : Box, Text, Button, Input, Select, Markdown, Code, Link. Le terminal sait en plus afficher une grille de cases colorées (Raster) et des images, l'app desktop du SVG.
Les réglages de l'utilisateur : userConfig
Un mod peut déclarer des réglages dans son plugin.json, sous userConfig. Claude Code les demande à l'installation, les affiche comme des lignes de /config, et les passe à register dans options, valeurs par défaut comprises. L'exemple 2 s'en sert pour la durée du cache.
Exemple 2 : ta conso sous le prompt, sans tour de Claude
Le besoin : voir en permanence où en est ta session, et surtout savoir si ton cache est encore chaud avant de relancer Claude après une pause. La reprise à froid peut coûter 25 à 40 fois le prix d'un message (chapitre 19).
Le mod lit ce que Claude Code connaît déjà, sans appeler de modèle :
$.session.usage(), gratuit sans argument : le remplissage du contexte (context.percent), et aussi les fenêtres de limite (rateLimits, vide sur clé API) et le coût de la session ;e.usageà chaqueturn.complete: les tokens du tour, dontcache_read_input_tokens(lus en cache) etcache_creation_input_tokens(écrits en cache) ;$.clock.now(), pour le temps écoulé depuis la fin du dernier tour.
La durée de vie du cache dépend de ton mode de paiement : 1 heure pour la conversation principale sur abonnement dans ton quota, 5 minutes sur clé API ou crédits d'usage. Le mod la demande donc avec userConfig. .claude-plugin/plugin.json :
{
"name": "conso",
"version": "0.1.0",
"description": "Contexte, part lue en cache et état du cache sous le prompt, sans tour de Claude",
"author": { "name": "Ton nom" },
"userConfig": {
"cache_ttl_minutes": {
"type": "number",
"title": "TTL du cache (minutes)",
"description": "60 sur abonnement dans ton quota, 5 sur clé API ou crédits d'usage",
"default": 60,
"min": 1,
"max": 60
}
}
}
hooks/register.ts :
import type { EngineInterface, Register } from "claude-code";
let ttlMin = 60; // main-conversation cache TTL, from userConfig
let lastAt = 0; // when the last main-conversation turn ended
let cached = 0; // % of that turn's input read from the cache
// Reads figures the engine already holds: no model call, no tokens
async function summary($: EngineInterface): Promise<string> {
const { context } = await $.session.usage();
const left = Math.ceil(ttlMin - ((await $.clock.now()) - lastAt) / 60_000);
const cache = left > 0 ? `cache chaud ~${left} min` : "cache froid";
return `contexte ${context.percent ?? "?"} % · lu en cache ${cached} % · ${cache}`;
}
export const register: Register = (on, options) => {
ttlMin = Number(options.cache_ttl_minutes ?? 60);
on("session.start", async ($, e, next) => {
// Refresh the line every 30 s, so "cache froid" shows up on its own
$.clock.every(30_000, async () => $.ui.status(await summary($)));
await $.command.register({
name: "conso",
description: "Ta conso, sans tour de Claude",
immediate: true,
});
return next(e);
});
on("turn.complete", async ($, e, next) => {
if (!e.agentId && e.usage) {
const { cache_read_input_tokens: read, ...u } = e.usage;
const total = read + u.input_tokens + u.cache_creation_input_tokens;
cached = Math.round((100 * read) / Math.max(1, total));
lastAt = await $.clock.now();
$.ui.status(await summary($));
}
return next(e);
});
on("command.run", { command: "conso" }, async ($) => {
$.ui.toast(await summary($), { timeoutMs: 8000 });
return {}; // {} prints nothing, so the conversation stays as it was
});
};
Ce que tu vois, sous le prompt, après chaque tour et toutes les 30 secondes (Claude Code préfixe la ligne du nom du mod) :
contexte 8 % · lu en cache 90 % · cache chaud ~60 min
Deux détails comptent :
!e.agentId:turn.completese déclenche aussi pour les tours des subagents. On ne suit que la conversation principale.immediate: true:/consorépond même pendant que Claude travaille, sans attendre la fin du tour.
Pour le reste, return {} n'ajoute rien à la conversation, la ligne de $.ui.status s'ajoute sans remplacer ta status line, et summary est au niveau du fichier pour que l'analyse statique accepte qu'on lui passe $.
Le « cache chaud » est une estimation qui part de la fin du dernier tour, alors que le cache court depuis la dernière requête ou réponse : pour le taux réel, /usage et sa ligne Prompt cache (main) restent la référence.
$ claude plugin validate ./conso
❯ ./register.ts hooks: session.start, turn.complete, command.run{command=conso}
❯ ./register.ts answers its own command: command.run{command=conso}
❯ ./register.ts calls: $.clock.every, $.clock.now, $.command.register, $.session.usage (via summary), $.ui.status, $.ui.toast
✔ Validation passed
Installer, activer, désactiver, tester
Tester sans session
claude plugin test lance les fichiers *.test.ts du dossier contre le vrai moteur, sans session, sans connexion et sans réseau. Le test déclenche les événements, et des « stubs » (de fausses réponses) répondent à la place de Claude Code. Celui-ci simule ta réponse au garde-fou :
import { expect, test } from "claude-code/testing";
test("rm -rf is refused when the user says no", async ($, on) => {
// $.ui.ask reaches the stubs as a call to the AskUserQuestion tool
on("tool.call", ($, e) =>
e.tool === "AskUserQuestion" && e.questions
? { result: { answers: { [e.questions[0]!.question]: "Refuser" } } }
: { result: "ran" },
);
const r = await $.tool.call({ tool: "Bash", command: "rm -rf build" });
expect(r.deny).toContain("refusé");
});
$ claude plugin test
tests/garde-fou.test.ts:
(pass) rm -rf is refused when the user says no [15.01ms]
(pass) a harmless command runs without a question [6.00ms]
(pass) a force push to main runs when the user says yes [6.56ms]
(pass) a force push to another branch passes [5.55ms]
4 pass
0 fail
Le kit fournit aussi mock.clock(on), une horloge que le test avance à la main, pour vérifier que conso passe à « cache froid » au bout de 61 minutes sans les attendre.
Astuce : lancé depuis un dossier sans mod, claude plugin test te dit si les mods peuvent tourner chez toi. no hooks module to load : oui. hooks modules are turned off here : un réglage les bloque.
Charger, installer, couper
# une session, depuis un dossier, avec rechargement à chaque sauvegarde
claude --plugin-dir ./conso
# installer depuis un marketplace (dans le terminal ou en session avec /plugin install)
claude plugin install conso@ton-marketplace
# couper sans désinstaller, puis rallumer
claude plugin disable conso@ton-marketplace
claude plugin enable conso@ton-marketplace
Un mod installé ou mis à jour depuis le shell pendant une session se charge avec /reload-plugins. Un plugin installé tourne depuis une copie mise en cache par version : tes modifs ne l'atteignent qu'avec une nouvelle version. Développe avec --plugin-dir, installe quand c'est stable.
Pour partager un mod, ajoute un .claude-plugin/marketplace.json à son repo GitHub, puis donne cette ligne, à taper dans une session :
/plugin install conso --marketplace ton-compte/ton-repo
Pour couper les mods plus largement :
- un seul : désactive-le dans
/plugin, onglet Installed ; - tous, pour une session :
claude --safe-mode, qui coupe aussi tes autres personnalisations ; - tous, partout :
"disableAllHooks": truedans~/.claude/settings.json, qui arrête aussi tes hooks shell et ta status line.
Ces réglages n'arrêtent pas les mods intégrés. Et si tu avais posé CLAUDE_CODE_ENABLE_FUNCTION_HOOKS pendant la phase de test, retire-le : il est ignoré depuis la v2.1.287.
Avant d'installer le mod de quelqu'un d'autre
D'après la doc d'Anthropic, un mod chargé a accès à :
- ta machine, en ton nom : il lit et écrit tes fichiers, lance des programmes, fait des requêtes réseau ;
- tes secrets, puisque les variables d'environnement et les fichiers de réglages contiennent souvent des clés d'API ;
- toute ta session, chaque prompt et chaque appel d'outil, qu'il peut réécrire, et il peut même envoyer un prompt comme si tu l'avais tapé ;
- tes permissions : il peut approuver un appel d'outil avant que la question te soit posée, y compris un appel qu'une règle
askt'aurait soumis ou qu'un de tes hooksPreToolUseavait bloqué ; - ton quota, dès qu'il appelle un modèle.
Et peu de choses le retiennent :
- pas de sandbox. Le sandboxing de Claude Code isole les commandes Bash de Claude, pas un processus lancé par un mod ;
- tes règles
denyne tiennent face à un mod que si le garde intégrésec-defaultest chargé, ce qui arrive avec des réglages gérés ou un compte Team ou Enterprise. Sur une clé API ou un compte Pro ou Max perso, un mod peut approuver un appel qu'une règledenyrefuse ; - même avec le garde, ces règles ne couvrent que les outils de Claude, pas les
$.fset$.processdu mod.
Comme tout passe par $, Claude Code peut te dire ce qu'un mod fait sans l'exécuter. Clone le repo et lance :
claude plugin validate ./le-mod
Lis les lignes hooks: et calls:. Ce qui doit t'arrêter :
| Dans la sortie | Ce que ça veut dire |
|---|---|
$.fs.read, $.fs.write | Lit ou écrit n'importe où, avec tes droits |
$.process.run, $.process.spawn | Lance des programmes |
$.http.fetch | Parle au réseau |
$.env.get, $.settings.read | Lit tes variables et réglages. La ligne env reads: nomme chaque variable |
$.model.complete | Dépense ton quota ou ta clé API |
$.prompt.submit, $.session.send | Envoie des messages à Claude, ou à une autre de tes sessions |
tool.check dans hooks: | Peut approuver ou refuser un appel avant la fenêtre de permission |
session.append dans hooks: | Peut réécrire chaque ligne de la conversation avant qu'elle soit enregistrée |
Un mod d'affichage qui demande $.http.fetch et $.env.get, c'est une question à poser à son auteur. La routine du chapitre 16 (Claude Code à jour, une installation à la fois, version épinglée) s'applique aussi.
Ce que ça change sur ta conso
Le code d'un mod tourne sur ta machine. Une commande qu'il sert, une ligne de statut, un toast, un panneau ou une minuterie ne lancent aucun tour de Claude et n'appellent aucun modèle : zéro token. Les exceptions :
$.model.completeet$.model.forkfacturent sur ton abonnement ou ta clé API.forkrepart de la conversation en cours et profite du cache tant qu'il est chaud ;$.prompt.submitlance un vrai tour ;- ce que le mod renvoie à Claude (le
{ text }d'une commande, le motif d'undeny) entre dans la conversation, donc garde-le court ; - un hook sur
prompt.sectionouprompt.contextqui change le texte d'une requête à l'autre casse le cache du prompt système ; $.session.usage({ breakdown: 'full' })envoie des requêtes de comptage de tokens, comme/context. L'appel sans argument est gratuit.
Le détail des leviers de coût est au chapitre 19.
Un mod, ou quelque chose de plus simple ?
Un mod sert quand tu as besoin de l'intérieur de Claude Code : dessiner, retenir un appel le temps d'une question, répondre à une commande sans tour, réagir à l'état de la session. Sinon, plus simple suffit. Une règle de permission comme Bash(npm test) autorise ou interdit une commande fixe sans code (chapitre 9). Un hook shell fait l'affaire si tu as déjà le script ou si tu veux juste bloquer, laisser passer ou journaliser. Si Claude ne sait pas faire, écris un skill, et s'il doit parler à un système externe, un serveur MCP (ton mod pourra l'appeler avec $.mcp).
Pour trancher vite : si ta phrase finit par « et je veux le voir à l'écran » ou « sans que Claude ait à y penser », pars sur un mod.
Statut early access
Entre la v2.1.287 et la v2.1.296, presque chaque version a ajouté ou corrigé quelque chose ($.ui.notify en 2.1.295, isDeferred sur les outils en 2.1.293). Note dans le README de ton mod la version de Claude Code testée, et fie-toi aux déclarations TypeScript de ta version plutôt qu'à ce chapitre. Anthropic peut couper les mods installés à distance (claude plugin test affiche alors the rollout switch served off), et une organisation peut les limiter aux siens avec allowManagedModsOnly.
Question
Ton garde-fou sur rm -rf lève une erreur au milieu de son hook tool.call, avant d'avoir appelé next. Il n'a pas de .catch. Que se passe-t-il ?
Choisis une réponse pour voir l'explication.
À retenir
- Un mod est un plugin dont le code (JS ou TS) tourne dans Claude Code : il observe, réécrit ou répond aux événements avec
($, e, next), et tout ce qu'il fait au-dehors passe par$. - Pour en faire un, décris-le à Claude ou lance
/plugin-authoring. Vérifie avecclaude plugin validateetclaude plugin test, développe avec--plugin-dir. - Il tourne avec tes droits, sans sandbox. Avant d'installer celui d'un autre, lis ses lignes
hooks:etcalls:. - Ce qu'il fait sans modèle (commande, ligne de statut, panneau) ne coûte aucun token.
$.model.completeet$.prompt.submitcoûtent.
Sources
- Mods overview, Create a mod, React to events, Use the mods API, Mods reference (doc Claude Code)
- Test a mod, Troubleshoot a mod, Manage mods for your organization (doc Claude Code)
- How Claude Code uses prompt caching (doc Claude Code), pour la durée de vie du cache
- Changelog de Claude Code, versions 2.1.287 à 2.1.296
- Source des mods intégrés et mods d'exemple (GitHub, Anthropic)
- Le skill intégré
plugin-authoringde Claude Code 2.1.296 et ses déclarationsclaude-code.d.ts, pour les noms exacts de l'API