SpyBara
Go Premium

plugins/mods/api.md 2026-09-30 23:00 UTC to 2026-10-01 23:02 UTC

This page contains 218 additions and 0 deletions.

2026
Thu 1 23:02

Utiliser l'API mods

Appelez l'API mods à partir d'un mod Claude Code pour ajouter des commandes et des outils, appeler un modèle, exécuter du travail sur un minuteur, envoyer des messages à d'autres sessions et accéder aux fichiers et au réseau.

L'API mods est l'ensemble des méthodes qu'un mod appelle pour agir : ajouter des commandes et des outils, appeler un modèle, exécuter du travail entre les événements et accéder au système de fichiers, aux processus et au réseau. Chaque hook la reçoit comme premier argument, $, avec les méthodes regroupées dans des espaces de noms tels que $.ui et $.fs. Les événements décident quand un hook s'exécute, et l'API mods est ce que le hook appelle une fois qu'il le fait.

Créez votre premier mod avant de commencer ici. Pour chaque méthode, consultez les méthodes de l'API mods ou lisez les types pour votre build.

Ajouter une commande ou un outil

Un mod peut ajouter une commande que l'utilisateur peut exécuter et un outil que Claude peut appeler. Enregistrez les deux dans un hook session.start. Claude Code attend ce hook avant la première invite, donc ce que vous enregistrez est disponible dès le premier tour.

Ajouter une commande

Une commande est destinée à l'utilisateur. Enregistrez-la, puis gérez command.run pour son nom. Cet exemple ajoute une commande /standup qui prend un nombre de jours facultatif :

on('session.start', async ($, e, next) => {
  // Add /standup to the command list, with the description the user sees there
  await $.command.register({ name: 'standup', description: 'Summarize what changed today', argumentHint: '[days]' })
  return next(e)
})

// The matcher limits the hook to /standup, so other commands don't reach it
on('command.run', { command: 'standup' }, async ($, e) => {
  // e.args is the text typed after the command name, or an empty string
  return { text: 'Summary for the last ' + (e.args || '1') + ' day(s): ...' }
})

Après le démarrage de la session, /standup apparaît avec sa description dans la liste que vous voyez quand vous tapez /. Le argumentHint s'affiche dans l'invite après que vous ayez tapé la commande et un espace, comme dans /standup [days]. Quand vous exécutez /standup 3, le deuxième hook retourne Summary for the last 3 day(s): ..., et la transcription affiche ce texte après le nom du plugin. Le hook n'appelle jamais next, car la commande n'a pas d'autre comportement que le vôtre.

Le text que vous retournez s'affiche dans la transcription et Claude le lit. Pour ne rien imprimer, comme une commande qui ouvre seulement un volet, retournez {}. Pour laisser la commande s'exécuter pendant que Claude travaille, ajoutez immediate: true à l'enregistrement.

Choisissez un nom qu'aucune commande intégrée n'utilise. Tapez / dans une session pour les voir. $.command.register lève une exception pour un nom pris, avec un message tel que "/focus" refused: it is the built-in /focus". Un hook qui lève une exception est ignoré, donc le reste de votre hook session.start ne s'exécute pas non plus. Enregistrez les commandes en dernier dans ce hook, ou enveloppez l'appel dans try et catch.

Ajouter un outil

Un outil est destiné à Claude. Enregistrez-le avec un nom, une description que Claude lit, et un schéma JSON pour son entrée. Claude le voit sous un nom plus long composé de mcp__, du nom de votre plugin, de deux traits de soulignement et du nom que vous avez enregistré. Vous gérez ses appels dans un hook tool.call filtré sur ce nom complet. Cet exemple, d'un plugin nommé my-mod, enregistre ticket, donc le nom complet est mcp__my-mod__ticket. Il donne à Claude un outil qui recherche un ticket dans un suivi de problèmes :

on('session.start', async ($, e, next) => {
  await $.tool.register({
    name: 'ticket',
    // Claude decides when to call the tool from this description
    description: 'Look up a ticket by its id and return its title and status',
    // The arguments Claude has to send: one required string named id
    inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
  })
  return next(e)
})

// The full tool name is mcp__, the plugin's name, and the registered name
on('tool.call', { tool: 'mcp__my-mod__ticket' }, async ($, e) => {
  // The tool's arguments are fields of e, so the id is e.id
  const response = await $.http.fetch('https://tickets.example.com/api/' + encodeURIComponent(e.id))
  // Return a result either way, so Claude learns when the lookup failed
  return { result: response.ok ? response.text : 'Lookup failed with status ' + response.status }
})

Quand vous posez une question sur un ticket, Claude peut appeler mcp__my-mod__ticket avec son id. Le deuxième hook récupère le ticket et retourne le corps de la réponse, que Claude lit comme le résultat de l'outil. Quand le serveur répond avec un statut d'erreur, Claude lit Lookup failed with status et le numéro.

Appeler un modèle

Un mod peut poser une question à un modèle de son propre chef, en dehors de la conversation, pour une petite tâche comme trier ou résumer un morceau de texte. $.model.complete envoie une invite à un modèle avec les identifiants de votre session et se résout en la réponse. Il n'a pas d'historique de conversation.

Ce hook répond à une commande /triage, enregistrée comme une commande, en demandant à un petit modèle d'étiqueter le texte tapé après :

on('command.run', { command: 'triage' }, async ($, e) => {
  const r = await $.model.complete({
    model: 'haiku',
    // The system prompt sets the job, and the prompt carries the text to label
    system: 'Reply with one word: bug, feature, or question.',
    prompt: e.args,
    // One word needs few tokens, and the call gives up after 15 seconds
    maxTokens: 20,
    timeoutMs: 15000,
  })
  // r.text exists only when the model answered, so check r.isAnswered first
  const label = r.isAnswered ? r.text.trim() : 'unknown'
  return { text: 'Label: ' + label }
})

Quand vous exécutez /triage the export button does nothing, le mod envoie ce texte au modèle et affiche sa réponse, comme Label: bug. La conversation de Claude ne fait pas partie de la demande. Quand le modèle ne répond pas, l'étiquette est unknown.

Une défaillance de l'API Claude ne rejette pas l'appel, donc vérifiez r.isAnswered, et lisez r.reason quand c'est false. L'appel rejette seulement pour une demande que Claude Code n'enverra pas, comme un modèle que votre organisation bloque. Les types pour votre build listent les autres options, comme effort, et les limites donnent la valeur par défaut de maxTokens.

$.model.fork({ prompt }) pose une question sur la conversation actuelle à la place, avec le même modèle et la même invite système, donc l'API Claude sert la plupart de celle-ci à partir du cache d'invite.

Ces appels utilisent le plan ou la clé API de l'utilisateur.

Exécuter du travail en arrière-plan

Le travail qui dépasse un événement, comme vérifier quelque chose une fois par minute, s'exécute sur un minuteur que vous démarrez à partir de session.start. Un hook lui-même s'exécute pour un événement et a une limite de temps de 10 secondes de son propre temps d'exécution. Le temps passé à attendre next ou un appel de l'API mods ne compte pas, sauf un $.clock.sleep. $.clock.every et $.clock.after remplacent setInterval et setTimeout, avec le délai en millisecondes en premier : $.clock.after(5000, fn) appelle fn une fois, cinq secondes à partir de maintenant. Chacun retourne un minuteur avec une méthode cancel(), et await $.clock.now() donne l'heure en millisecondes.

Ce hook recherche les vérifications d'une demande de tirage une fois par minute et affiche le résultat sous l'invite. summarize est une fonction de votre propre création qui transforme la sortie JSON de la commande en quelques mots :

on('session.start', async ($, e, next) => {
  // Call the function every 60,000 milliseconds, starting one minute from now
  $.clock.every(60_000, async () => {
    const status = await $.process.run(['gh', 'pr', 'checks', '--json', 'state'])
    // Replace the line under the prompt with the latest summary
    $.ui.status('checks: ' + summarize(status.stdout))
  })
  // Return without waiting for the timer, so the session starts right away
  return next(e)
})

La session démarre comme d'habitude. Une minute plus tard, une ligne apparaît sous l'invite avec un ⚠, le nom du mod, puis checks: et votre résumé. Elle est remplacée une fois par minute après cela. Le rappel du minuteur s'exécute en dehors de tout événement, donc il continue de s'exécuter entre les tours et n'en démarre pas un. Si le rappel lève une exception, l'erreur va au journal de débogage et le minuteur s'exécute à nouveau à l'intervalle suivant.

Afficher quelque chose sans démarrer un tour

Un travail en arrière-plan peut afficher quelque chose à l'utilisateur sans démarrer un tour. Chacun de ces appels met du texte à un endroit différent :

Appel Ce que l'utilisateur voit
$.ui.status(text) Une ligne sous l'invite qui reste jusqu'à ce que vous la changiez. Elle commence par ⚠ et le nom du mod, comme dans ⚠ my-mod: checks: 3 passing.
$.ui.toast(text) Une petite boîte en haut à droite, avec le nom du mod au-dessus du texte, qui disparaît après quelques secondes
$.ui.log(text) Une ligne atténuée dans la transcription que Claude ne lit pas. Elle commence par ● et le nom du mod, comme dans ● my-mod: build finished.

Démarrer un tour à partir d'un travail en arrière-plan

Quand un travail en arrière-plan trouve quelque chose qui nécessite l'attention de Claude, il peut démarrer un tour en soumettant une invite avec $.prompt.submit({ text }). Claude lit le texte après une phrase qui nomme votre mod comme l'expéditeur. Pour l'envoyer comme les propres paroles de l'utilisateur, sans cette phrase, ajoutez asUser: true. L'appel attend que la session soit inactive, puis démarre un nouveau tour. Il se résout quand ce tour démarre, donc ne l'await pas dans un gestionnaire qui s'exécute pendant que Claude travaille.

Arrêter le travail en arrière-plan

Le travail en arrière-plan s'arrête de deux façons. Les minuteurs s'arrêtent quand le module se recharge. Pour le travail de longue durée à l'intérieur d'un hook, next.signal est un AbortSignal qui s'interrompt quand l'événement que votre hook gère est abandonné, par exemple quand l'utilisateur interrompt, donc passez-le à tout ce qui est de longue durée.

Envoyer et recevoir des messages entre les sessions

Un mod peut envoyer un message en texte brut à une autre de vos sessions ou à l'un des sous-agents de cette session, et observer les messages qui arrivent et partent. $.session.send({ to, text }) en envoie un, la même livraison que l'outil SendMessage fait. to est { sessionId } pour une session, { agentId } pour un sous-agent de $.agent.list(), ou l'adresse de chaîne d'où provient un message reçu. L'appel se résout une fois que le message est mis en file d'attente, avec { isDelivered: true }. Quand rien n'a été livré, il se résout avec { isDelivered: false, reason }, et reason dit pourquoi.

Ce hook répond à une commande /ping, enregistrée comme une commande, en demandant à la session dont vous tapez l'id après un statut :

on('command.run', { command: 'ping' }, async ($, e) => {
  // e.args is the session id typed after /ping
  const sent = await $.session.send({ to: { sessionId: e.args }, text: 'Status? One line.' })
  // The call resolves either way, so check isDelivered to learn what happened
  if (!sent.isDelivered) $.ui.toast('Not delivered: ' + sent.reason)
  // An empty result prints nothing in this session's transcript
  return {}
})

Quand le message est mis en file d'attente, rien n'apparaît dans votre session, et Claude de l'autre session lit Status? One line. Quand rien n'a été livré, une petite boîte en haut à droite donne la raison et disparaît après quelques secondes.

Deux événements permettent à un mod d'observer les messages. Retournez next(e) des deux pour passer chaque message inchangé :

Événement Se déclenche quand Champs utiles
session.receive Un message arrive pour cette session, avant que Claude le lise e.text, et e.origin.kind, comme peer ou peer-send-message pour une autre session ou un agent, task-notification, ou scheduled-trigger. Retournez { consumed: reason } pour l'empêcher de Claude.
session.send Un message est sur le point de partir, de l'outil SendMessage ou d'un mod e.to, e.text, et e.origin.kind, qui est model ou plugin

Une session définie pour refuser les messages entrants refuse un message avant que session.receive se déclenche, donc un hook ne le voit jamais. Un message qui est retenu pour votre approbation atteint d'abord le hook, donc un mod peut lire un message que vous n'avez pas encore approuvé. Le next(e) du hook rejette quand le message n'est pas livré.

Le nom de l'expéditeur sur un message reçu est ce que l'expéditeur a écrit, donc ne basez pas une décision sur celui-ci.

Accéder aux fichiers, processus et au réseau

Un mod accède au système de fichiers, aux processus et au réseau via l'API mods, avec les mêmes permissions que l'utilisateur exécutant Claude Code. Le module hooks lui-même n'a pas d'API Node.js, pas de globales de minuteur comme setTimeout, et pas d'accès réseau ou fichier de son propre chef. Les API JavaScript standard et web comme URL, TextEncoder, AbortController, et crypto.subtle sont disponibles. Chaque espace de noms ci-dessous couvre un type d'accès :

Espace de noms Ce qu'il fait
$.fs read(path), write(path, text), exists(path), stat(path), et list(path) fonctionnent sur les fichiers et répertoires
$.process run(['git', 'status']) démarre une commande et se résout quand elle se termine. spawn diffuse la sortie d'une commande de longue durée.
$.http fetch(url, init) sur http ou https. Il se résout en { status, ok, headers, text } une fois le corps lu.
$.store Un magasin de paires clé-valeur JSON de votre propre plugin, conservé entre les sessions
$.env get et set les variables d'environnement. Écrivez le nom comme une chaîne littérale.
$.settings read ce que les fichiers de paramètres et la politique gérée contiennent
$.session messages() retourne la transcription comme une liste de { role, text, toolUses }. Aussi le répertoire de travail, le modèle, et plus. usage() retourne l'utilisation de la fenêtre de contexte et les limites du plan.
$.mcp call un outil sur un serveur MCP connecté

Les fichiers et processus ont quelques règles qui leur sont propres :

  • Chemins : un chemin relatif est sous le répertoire de travail de la session
  • $.fs.list : retourne les entrées d'un répertoire comme { name, kind, size, isLink } et ne descend pas dans les sous-répertoires
  • $.process.run : prend une liste d'arguments et n'utilise pas de shell. Il se résout en { exitCode, stdout, stderr } quel que soit le code de sortie. Il rejette si le programme ne peut pas démarrer ou s'exécute toujours au délai d'expiration, qui est de 30 secondes par défaut, donc enveloppez-le dans try et catch.

Chacun de ces appels est lui-même un événement, nommé pour son espace de noms et sa méthode sans le $., comme fs.read pour $.fs.read. Un mod plus tôt dans la chaîne peut observer, réécrire ou refuser votre appel, c'est ainsi qu'une organisation restreint ce que les mods atteignent.

Prochaines étapes