Gaëtan Wittebolle.
UmamiContactEN
EN
← guides
Claude Code, le guide
Chapitre 20 / 22
  • 15Créer des visuels : artifacts, Design, Slides et images
  • 16Les plugins et le marketplace
  • 17MCP, brancher Claude à ton SaaS
  • 18Laisser Claude travailler seul : /goal, /loop, routines et workflows
  • 19Optimiser coûts et tokens : le cache, le modèle et le suivi
  • 20Les mods : modifier Claude Code lui-même
  • 21Ma configuration complète

Partie 6 · Bonus, configuration d'un power user

Les mods : modifier Claude Code lui-même

Chapitre 20 · 10 min de lecture

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.

ModClaude Mods, function hooks

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 /quelquechose qui 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 CodeLes hooks du mod tournentCe qu'il dessine s'affiche
claude dans un terminal (y compris celui de ton éditeur)OuiOui
Onglet Code de l'app desktopOuiOui, sauf les éléments réservés au terminal
Session WSL dans l'app desktopNonNon
Panneau de chat de l'extension VS CodeOuiNon
claude -p et l'Agent SDKOuiNon
Remote Control depuis claude.ai ou le mobileOui, sur ta machineDans le terminal de ta machine
Session cloudOui, si le plugin y estNon

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.

Hook de réglagessettings hook, hook shell

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.

Schéma qui compare plugin, skill, hook de réglages, serveur MCP et mod : le plugin emballe les quatre autres, et seul le mod tourne dans le processus de Claude Code et dessine dans l'interface. Pour chacun, le schéma indique ce qui le déclenche, ce qu'il peut changer et ce qu'il coûte en tokens.
Plugin, skill, hook, MCP, mod : qui fait quoi.
ModHook de réglagesSkillServeur MCP
Ce que c'estDes fonctions que Claude Code appelle dans son propre processusUne commande shell, une requête HTTP ou un prompt sur un événementUn fichier SKILL.md d'instructionsUn processus externe qui donne des outils
Ce qu'il peut changerAppels d'outils, prompts, commandes, tours, ce que l'interface dessineLaisser passer ou bloquer, modifier les arguments, ajouter du contexteCe que Claude sait et faitLes outils dont Claude dispose
Dessine dans l'interfaceOuiNonNonNon
Ce que tu écrisJavaScript ou TypeScriptUn script et une entrée dans settings.jsonDu markdownUn serveur, dans le langage que tu veux
Choisis-le quandTu veux un panneau, un bandeau, une commande à toi, ou réécrire un événementTu veux bloquer, autoriser ou journaliser avec un script existantTu recolles les mêmes consignes dans le chatClaude 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.

  1. Décris le mod. /plugin-authoring puis 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.
  2. 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.
  3. 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, lance claude plugin validate, et tu testes dès la fin du tour.
  4. 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 avec claude --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.

Hooks modulele code du mod

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.
Schéma d'un mod : trois fichiers (plugin.json, hooks.json et le hooks module qui exporte register), puis un événement de Claude Code qui passe le matcher, entre dans ton hook ($, e, next) et en ressort laissé passer, réécrit, refusé ou dessiné. Le module tourne isolé, sans Node ni DOM, et un hook qui plante est sauté.
Anatomie d'un mod : trois fichiers, puis un événement qui traverse ton hook.

Ce que ton hook renvoie détermine l'effet :

Ton hook renvoieEffet
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 codeAgir 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 :

  1. le garde intégré sec-default et les mods de ton organisation placés en tête ;
  2. les mods que tu as installés ;
  3. les mods de l'organisation placés en queue ;
  4. les autres mods intégrés à Claude Code.

Expliqué simplement

Pense à une file de contrôle. Chaque hook reçoit l'événement et choisit : le passer au suivant avec next, le modifier avant, ou l'arrêter là. Le premier de la file voit l'événement avant tous les autres et récupère le résultat en dernier, donc il décide si les suivants tournent.

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(...), jamais const fs = $.fs ni 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énementsExemple d'usage
Outilstool.call, tool.check, tool.describeBloquer un git push sur main, répondre à la place d'un outil
Promptsprompt.submit, prompt.section, prompt.context, prompt.mention, skill.promptAjouter la branche courante quand tu parles de PR
Commandes et réglagescommand.run, command.describe, config.set, config.describeServir ta propre commande, refuser un changement de /config
Toursturn.start, turn.step, turn.completeLire les tokens de chaque requête, envoyer une requête à un autre modèle
Sessionsession.start, session.end, session.compact, session.measure, session.append...Enregistrer tes commandes au démarrage, suivre le contexte
Subagentsagent.offer, agent.spawnChoisir le modèle d'un subagent, en masquer un
Interfaceui.render, ui.press, ui.input, ui.select, ui.close...Dessiner un panneau, réagir à un bouton
Autres modsplugin.register, engine.createRefuser un mod au chargement (politique d'entreprise)
Hooks de réglagesclassic.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

EspaceCe qu'il permet
$.uiOuvrir 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
$.commandEnregistrer une commande, en lancer une, lister les commandes
$.toolDéclarer un outil que Claude peut appeler, en appeler un
$.modelcomplete (un prompt seul, au modèle de ton choix), fork (une question posée sur la conversation en cours), classify
$.promptSoumettre un prompt (ça lance un tour), remplir le champ de saisie, lire ce que tu tapes
$.sessionLa conversation (messages), le dossier, le modèle, la conso (usage), compacter, envoyer un message à une autre session
$.agentDéclarer un type de subagent, en lancer un, lister ceux qui tournent
$.fs, $.process, $.httpLire et écrire des fichiers, lancer une commande (sans shell), faire une requête réseau
$.store, $.stateGarder des valeurs entre sessions (4 Mio de JSON au total), ou un état réactif qui redessine l'interface quand il change
$.clockL'heure, et des minuteries (every, after) qui remplacent setInterval et setTimeout
$.env, $.settings, $.configLire ou poser une variable d'environnement, lire les réglages, les lignes de /config
$.mcpAppeler 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 à chaque turn.complete : les tokens du tour, dont cache_read_input_tokens (lus en cache) et cache_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.complete se déclenche aussi pour les tours des subagents. On ne suit que la conversation principale.
  • immediate: true : /conso ré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": true dans ~/.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 ask t'aurait soumis ou qu'un de tes hooks PreToolUse avait 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 deny ne tiennent face à un mod que si le garde intégré sec-default est 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ègle deny refuse ;
  • même avec le garde, ces règles ne couvrent que les outils de Claude, pas les $.fs et $.process du 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 sortieCe que ça veut dire
$.fs.read, $.fs.writeLit ou écrit n'importe où, avec tes droits
$.process.run, $.process.spawnLance des programmes
$.http.fetchParle au réseau
$.env.get, $.settings.readLit tes variables et réglages. La ligne env reads: nomme chaque variable
$.model.completeDépense ton quota ou ta clé API
$.prompt.submit, $.session.sendEnvoie 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.complete et $.model.fork facturent sur ton abonnement ou ta clé API. fork repart de la conversation en cours et profite du cache tant qu'il est chaud ;
  • $.prompt.submit lance un vrai tour ;
  • ce que le mod renvoie à Claude (le { text } d'une commande, le motif d'un deny) entre dans la conversation, donc garde-le court ;
  • un hook sur prompt.section ou prompt.context qui 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 avec claude plugin validate et claude 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: et calls:.
  • Ce qu'il fait sans modèle (commande, ligne de statut, panneau) ne coûte aucun token. $.model.complete et $.prompt.submit coû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-authoring de Claude Code 2.1.296 et ses déclarations claude-code.d.ts, pour les noms exacts de l'API
← Chapitre 19

Optimiser coûts et tokens : le cache, le modèle et le suivi

Chapitre 21 →

Ma configuration complète