Référence : les hooks
Le bloc
hooks:du fichier de configuration : les événements, le JSON qu'un hook lit et écrit, l'environnement dans lequel il s'exécute, et ce que chaque décision fait au tour.
Configuration
hooks:
preGeneration: [<hook>, …]
preToolCall: [<hook>, …]
postToolCall: [<hook>, …]
postGeneration: [<hook>, …]
preAsk: [<hook>, …]
postAsk: [<hook>, …]
Chaque événement prend une liste ; les hooks d'un événement s'exécutent dans l'ordre où ils sont écrits. Un événement absent, ou une liste vide, n'exécute rien. Pas de bloc hooks: = aucun hook.
Entrée d'un hook
| Clé | Type | Défaut | Description |
|---|---|---|---|
command |
chaîne | — (obligatoire) | Exécutée par sh -c "<command>". Ne doit pas être vide ni blanche. |
matcher |
chaîne (expression régulière, syntaxe Go) | "" (tous les outils ou agents) |
preToolCall et postToolCall : le hook s'exécute pour les outils dont le nom correspond. preAsk et postAsk : pour les agents appelés dont le nom (leur clé dans mmConfigs) correspond. Interdit sur preGeneration et postGeneration. Non ancrée : bash correspond aussi à my_bash ; ^bash$ ne correspond qu'à bash. |
timeout |
durée | 10s |
Durée maximale d'une exécution avant qu'elle soit tuée. 0 = la valeur par défaut. Doit être ≥ 0. |
onError |
fail | warn | ignore |
warn |
Ce que fait une exécution en échec (voir Échecs). |
Exemple
hooks:
preGeneration:
- command: node "$MM_CONFIG_DIR/hooks/no-secrets.js"
timeout: 5s
onError: fail
preToolCall:
- matcher: ^(bash|write_file|edit_file)$
command: ./hooks/guard.sh
postGeneration:
- command: python3 "$MM_CONFIG_DIR/hooks/log.py"
onError: ignore
Événements
| Événement | S'exécute | Outils concernés | Peut |
|---|---|---|---|
preGeneration |
une fois par question, avant le premier appel au modèle | — | réécrire la question, ajouter du contexte, arrêter le tour |
preToolCall |
avant chaque appel d'outil, avant la demande d'autorisation de l'outil | intégrés et MCP, filtrés par matcher |
refuser l'appel, remplacer ses arguments |
postToolCall |
après chaque appel d'outil exécuté | intégrés et MCP, filtrés par matcher |
ajouter du contexte après la sortie de l'outil |
postGeneration |
une fois le tour terminé : répondu, en échec, annulé ou arrêté par un hook | — | observer seulement |
preAsk |
dans l'agent qui appelle, une fois par passage de relais à un autre agent — l'outil ask_agent, /ask, /ask-<nom>, /ask-with-context-<nom>, mm --ask — avant que l'agent appelé soit démarré ou interrogé ; pour ask_agent, après la demande d'autorisation de l'appel |
les agents de mmConfigs, filtrés par matcher |
refuser le relais, réécrire la question ou la conversation envoyée, ajouter du contexte pour l'agent appelé |
postAsk |
dans l'agent qui appelle, une fois le tour de l'agent appelé terminé : répondu, en échec ou annulé ; avant que la réponse devienne la sortie d'ask_agent ou entre dans l'historique |
les agents de mmConfigs, filtrés par matcher |
remplacer la réponse que lit l'agent qui appelle, ajouter du contexte après elle |
Les six s'exécutent dans tous les front ends : le mode ligne, la fenêtre (-tui), un éditeur (-acp), -p. Un agent démarré par /ask, ask_agent ou mm --ask exécute les hooks de son propre fichier de configuration ; preAsk et postAsk sont ceux de l'agent qui appelle. Un hook preGeneration ne s'exécute pas sur le résumé d'une compression de contexte.
Sur le chemin d'ask_agent, l'ordre est : preToolCall → demande d'autorisation → preAsk → l'agent appelé (avec ses propres hooks) → postAsk → postToolCall. Sur /ask… et mm --ask : preAsk → l'agent appelé → postAsk.
Exécution
| Aspect | Valeur |
|---|---|
| Commande | sh -c "<command>" |
| Dossier de travail | celui de la session : le dossier où mm s'exécute, ou le cwd de la session de l'éditeur en ACP |
| Environnement | celui de mm, plus les deux variables ci-dessous |
| stdin | un objet JSON (Entrée), puis fin de fichier ; &, < et > y sont écrits tels quels (sans échappement \u0026), si bien qu'un hook peut chercher && ou > dans une commande par simple recherche de texte |
| stdout | vide, ou un objet JSON (Réponse) ; 1 Mio au plus |
| stderr | cité (300 premiers octets) dans la ligne d'échec quand l'exécution échoue ; ignoré sinon |
| Concurrence | les hooks d'un événement s'exécutent l'un après l'autre ; preToolCall/postToolCall peuvent s'exécuter en parallèle pour deux appels d'outil d'une même réponse du modèle |
Variables d'environnement
| Variable | Valeur |
|---|---|
MM_HOOK_EVENT |
l'événement : preGeneration, preToolCall, postToolCall, postGeneration, preAsk ou postAsk |
MM_CONFIG_DIR |
dossier absolu du fichier de configuration qui a déclaré le hook |
L'environnement de mm contient aussi MM_BIN (le chemin du mm en cours) et MM_CONFIG (son fichier de configuration, quand il en a lu un), que mm pose pour tous les processus qu'il lance — voir la référence de la ligne de commande.
Entrée
L'objet JSON écrit sur l'entrée standard du hook.
| Champ | Type | Événements | Description |
|---|---|---|---|
event |
chaîne | tous | le nom de l'événement |
cwd |
chaîne | tous | le dossier de travail de la session, absolu |
model |
chaîne | tous | la référence du modèle, <fournisseur>/<modèle> |
prompt |
chaîne | preGeneration, postGeneration |
la question telle que tapée — lignes @path et $skill comprises. Dans une chaîne, la question telle que le hook précédent l'a laissée. Dans postGeneration : la question que le modèle a lue, ou le marqueur [prompt blocked by a hook: <raison>] quand un hook a arrêté le tour. |
tool |
objet | preToolCall, postToolCall |
l'appel d'outil : id (la référence de l'appel, peut être vide), name, input (les arguments, tels que le modèle les a envoyés ou que le hook précédent les a remplacés) |
ask |
objet | preAsk, postAsk |
le passage de relais — voir L'objet ask |
output |
chaîne | postToolCall |
la sortie de l'outil, telle que le modèle la lira (en JSON quand l'outil renvoie autre chose que du texte) |
answer |
chaîne | postGeneration, postAsk |
postGeneration : le texte de la dernière réponse. postAsk : la réponse complète de l'agent appelé, avant toute troncature, ou telle que le hook précédent l'a remplacée. Vide s'il n'y en a pas. |
error |
chaîne | postGeneration, postAsk |
l'erreur du tour, ou du relais ; absent en cas de succès |
durationMs |
nombre | postAsk |
le temps pris par l'agent appelé, en millisecondes |
asked |
objet | tous | présent seulement dans un agent lui-même appelé par un autre : {"depth": <n>}, 1 pour un agent appelé par celui qu'une personne a démarré |
Les champs vides sont omis.
L'objet ask
| Champ | Type | Description |
|---|---|---|
agent |
chaîne | le nom de l'agent appelé, sa clé dans mmConfigs |
config |
chaîne | le fichier de configuration de l'agent appelé, absolu |
agentModel |
chaîne | <fournisseur>/<modèle> lu dans ce fichier ; absent quand il est illisible |
via |
chaîne | tool (l'outil ask_agent), command (/ask, /ask-<nom>, /ask-with-context-<nom>) ou cli (mm --ask, mm --ask-with-context) |
withContext |
booléen | si la conversation est envoyée avec la question |
depth |
nombre | la profondeur de l'agent qui appelle : 0 pour l'agent qu'une personne a démarré |
prompt |
chaîne | la question. Dans une chaîne preAsk, telle que le hook précédent l'a laissée ; dans postAsk, telle qu'elle a été envoyée. |
transcript |
chaîne | la conversation envoyée (withContext seulement), de la même façon ; absent sinon |
Exemples
{"event":"preGeneration","cwd":"/home/moi/projet","model":"llmman/hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M","prompt":"list the files"}
{"event":"preToolCall","cwd":"/home/moi/projet","model":"llmman/…","tool":{"id":"call_1","name":"bash","input":{"command":"rm -rf build"}}}
{"event":"postAsk","cwd":"/home/moi/projet","model":"llmman/…","ask":{"agent":"expert","config":"/home/moi/projet/agent.expert.yaml","agentModel":"llmman/…","via":"tool","withContext":false,"depth":0,"prompt":"Qu'est-ce qu'un inode ?"},"answer":"Un inode est …","durationMs":25374}
Réponse
L'objet JSON qu'un hook peut écrire sur sa sortie standard. Une sortie vide (ou faite seulement d'espaces) vaut {"action":"continue"}. Tous les champs sont facultatifs.
| Champ | Type | Lu sur | Description |
|---|---|---|---|
action |
chaîne | tous | continue (par défaut, y compris quand il est absent), stop, deny ou error — voir plus bas |
prompt |
chaîne | preGeneration, preAsk |
remplace la question — celle de l'utilisateur, ou celle envoyée à l'agent appelé. Absent = inchangée ; "" = une question vide. |
transcript |
chaîne | preAsk |
remplace la conversation envoyée à l'agent appelé. Absent = inchangée ; "" = n'en envoyer aucune. |
answer |
chaîne | postAsk |
remplace la réponse que lit l'agent qui appelle. Absent = inchangée. |
context |
chaîne | preGeneration, postToolCall, preAsk, postAsk |
du texte pour un modèle. preGeneration : ajouté après la question. postToolCall : ajouté après la sortie de l'outil. preAsk : ajouté à ce que reçoit l'agent appelé. postAsk : ajouté après la réponse que lit l'agent qui appelle. Chacun est introduit par la ligne [context added by a hook]. Blanc = ignoré. |
input |
toute valeur JSON | preToolCall |
remplace les arguments de l'appel d'outil. Absent = inchangés. |
reason |
chaîne | preGeneration (stop), preToolCall et preAsk (deny) |
la raison. |
response |
chaîne | preGeneration (stop) |
ce que l'utilisateur lit à la place d'une réponse. Absente ou blanche : ⛔ This request was blocked by a hook: <raison>. |
error |
chaîne | tous (error) |
le message d'échec du hook lui-même. |
Les champs lus sur un autre événement sont ignorés.
Actions
| Action | Permise sur | Effet |
|---|---|---|
continue |
tous | le tour continue, avec le prompt, le context et l'input de ce hook appliqués |
stop |
preGeneration |
le tour se termine sans appeler le modèle ; les hooks suivants de l'événement ne s'exécutent pas |
deny |
preToolCall, preAsk |
l'outil ne s'exécute pas, ou l'agent appelé n'est ni démarré ni interrogé ; les hooks suivants de l'événement ne s'exécutent pas |
error |
tous | un échec, traité selon onError |
toute autre valeur, ou stop/deny sur un autre événement |
— | un échec : action "<action>" is not valid for <événement> |
Effets sur le tour
preGeneration
| Décision | Le modèle lit | L'historique garde | L'utilisateur voit |
|---|---|---|---|
| aucun changement | la question | la question | — |
prompt et/ou context |
la question réécrite, puis chaque contexte | la question réécrite (la copie propre au front end n'est pas modifiée pendant le tour) | — |
stop |
rien : le modèle n'est pas appelé | [prompt blocked by a hook: <raison>] (ou [prompt blocked by a hook]) suivi de la response comme réponse |
la response, comme réponse |
En chaîne : chaque hook lit le prompt laissé par le précédent ; les contextes s'accumulent dans l'ordre des hooks.
preToolCall
| Décision | L'outil | Le modèle lit | L'utilisateur voit |
|---|---|---|---|
| aucun changement | s'exécute avec les arguments du modèle | la sortie | la ligne habituelle de l'outil |
input |
s'exécute avec les arguments remplacés | la sortie | la ligne habituelle de l'outil (avec les arguments remplacés) |
deny |
ne s'exécute pas, et ne demande pas d'autorisation | [denied by a hook: <raison>] comme sortie de l'outil |
mode ligne : 🪝 <outil> [denied by a hook: <raison>] ; fenêtre et éditeur : un appel d'outil en échec intitulé <outil>: <commande, chemin, URL ou agent, 60 caractères au plus> — denied by a hook (<outil> — denied by a hook quand l'entrée n'a rien de tout cela) |
L'historique garde l'appel d'outil tel que le modèle l'a écrit, même quand input a remplacé ses arguments.
postToolCall
Les context de tous les hooks qui correspondent sont réunis et ajoutés à la sortie : <sortie>\n\n[context added by a hook]\n<contexte>. La sortie de l'outil montrée à l'utilisateur ne change pas.
postGeneration
Aucun effet sur le tour. La réponse est décodée (une réponse invalide est un échec), puis ignorée.
preAsk
| Décision | L'agent appelé | L'utilisateur voit |
|---|---|---|
| aucun changement | reçoit la question (précédée de la conversation avec withContext) |
le relais habituel |
prompt, transcript, context |
reçoit la question et la conversation réécrites, puis chaque contexte | le relais habituel |
deny |
n'est ni démarré ni interrogé | ask_agent : en mode ligne 🪝 ask_agent [ask denied by a hook: <raison>], dans la fenêtre et l'éditeur un appel ask_agent en échec ; le modèle lit [ask denied by a hook: <raison>]. /ask… : la commande échoue avec ask denied by a hook: <raison>, l'historique ne change pas. mm --ask : [ask: ask denied by a hook: <raison>] sur stderr, code de sortie 1. |
En chaîne : chaque hook lit le prompt et le transcript laissés par le précédent ; les contextes s'accumulent dans l'ordre des hooks. Avec /ask…, l'historique garde la question telle qu'elle a été envoyée.
postAsk
| Décision | L'agent qui appelle lit | L'utilisateur voit |
|---|---|---|
| aucun changement | la réponse (la sortie d'ask_agent, ou le message [<agent>'s answer] de /ask…) |
la réponse, telle qu'elle s'est affichée |
answer |
le remplacement | la réponse telle qu'elle s'est affichée, puis la notice 🪝 postAsk: <agent>'s answer was replaced by a hook — the asking agent reads the new one ; le résumé de l'appel ask_agent indique <agent> answered; a hook replaced the answer (<n> characters) |
context |
la réponse, puis [context added by a hook] et chaque contexte |
la réponse, telle qu'elle s'est affichée |
La réponse s'affiche pour l'utilisateur pendant que l'agent appelé l'écrit : postAsk change ce que l'agent qui appelle lit et garde, jamais ce qui a été montré. Un answer sur un relais en échec (pas un relais annulé) remplace l'échec : l'agent qui appelle le lit comme la réponse, et /ask… garde l'échange.
Avec au moins un hook postAsk déclaré, mm --ask retient la réponse et ne l'écrit sur stdout qu'une fois les hooks exécutés (remplacement et contexte compris) ; sans hook postAsk, il la diffuse au fil de l'eau comme avant.
Échecs
Une exécution échoue quand :
| Échec | Message |
|---|---|
| la commande se termine avec un code non nul | exit status <n>: <stderr> |
elle dure plus que timeout |
timed out after <timeout> |
le tour est annulé pendant qu'elle s'exécute (preGeneration, preToolCall, postToolCall, preAsk) |
cancelled |
| stdout dépasse 1 Mio | stdout larger than 1048576 bytes |
| stdout n'est pas du JSON | invalid JSON on stdout: … |
action vaut error |
l'error de la réponse, ou the hook reported an error |
action est inconnue ou non permise sur l'événement |
action "<action>" is not valid for <événement> |
postGeneration et postAsk ne sont pas annulés avec le tour : ils s'exécutent aussi après une annulation (Échap, Ctrl-C, /abort), bornés par leur timeout.
La suite dépend d'onError :
onError |
preGeneration |
preToolCall, preAsk |
postToolCall, postGeneration, postAsk |
|---|---|---|---|
fail |
ligne d'échec, tour arrêté (response : ⛔ This request was blocked by a hook: hook failed: …) |
ligne d'échec, appel ou relais refusé (reason : hook failed: …) |
ligne d'échec, on continue |
warn |
ligne d'échec, hook suivant | ligne d'échec, hook suivant | ligne d'échec, on continue |
ignore |
hook suivant, sans rien dire | hook suivant, sans rien dire | on continue, sans rien dire |
La ligne d'échec est 🪝 <événement> hook \`: <échec>: sur stdout en mode ligne, en ligne grisée dans la conversation de la fenêtre, sur stderr (les journaux de l'agent dans l'éditeur) en ACP, sur stderr avecmm --ask`.
Affichage
Avec displayHooks: true, chaque exécution de hook est affichée — sinon, un hook qui fonctionne est invisible. Une ligne par exécution :
🪝 <n>. <événement> [<outil ou agent>] · <commande> → <décision>
| Partie | Valeur |
|---|---|
<n> |
le numéro de l'exécution dans le tour, à partir de 1 (mode ligne, -p, fenêtre) |
<outil ou agent> |
le nom de l'outil (preToolCall, postToolCall) ou le nom mmConfigs de l'agent interrogé (preAsk, postAsk) ; absent pour preGeneration et postGeneration |
<commande> |
la command du hook, telle qu'écrite dans la configuration |
<décision> |
continue, stop, deny, failed (l'exécution a échoué ; son onError a décidé de la suite), ou ce que la réponse a changé, joint par + : prompt replaced, transcript replaced, answer replaced, input replaced, context (<n> chars) |
Seuls comptent les champs que l'événement lit : un context renvoyé à postGeneration est affiché continue. Le texte d'un contexte n'est jamais affiché, seulement sa longueur en caractères.
| Interface | Où |
|---|---|
mode ligne, -p |
après la réponse, sous la ligne ⚙ (et la liste de displayCommands), dans la même couleur |
fenêtre (-tui) |
une notice par exécution sous la ligne ⚙ |
éditeur (-acp) |
un appel d'outil par exécution, de type other, intitulé 🪝 <événement> … → <décision> (sans numéro), envoyé à la fin du hook et terminé aussitôt ; en échec (failed) quand l'exécution a échoué. Hors historique : non rejoué par session/load. |
Les hooks exécutés hors d'un tour (/ask en mode ligne ou dans la fenêtre) ne sont pas listés. mm --ask et le -p d'un agent à structuredOutput ne listent rien.
Exemple
🤖 The capital of France is Paris.⚙ 0 command(s)
🪝 1. preGeneration · node "$MM_CONFIG_DIR/hooks/guards.js" → context (178 chars)
Bannière de démarrage
Quand au moins un hook est déclaré, la bannière se termine par | hooks: <événement>×<nombre> …, une entrée par événement qui a des hooks, dans l'ordre preGeneration, preToolCall, postToolCall, postGeneration, preAsk, postAsk :
mm v0.4.0 | config: ../agent.yaml | … | yolo: on | hooks: preGeneration×2 preToolCall×1 postToolCall×1 postGeneration×1
Erreurs de validation
Signalées au démarrage sous la forme [config error: <chemin>: <message>], code de sortie 1.
| Message | Cause |
|---|---|
hooks: unknown event "<nom>" (want one of preGeneration, preToolCall, postToolCall, postGeneration, preAsk, postAsk) |
une clé de hooks: qui n'est pas un événement (une faute de frappe : preToolcall) |
hooks.<événement>[N]: command must not be empty |
pas de command, ou une commande blanche |
hooks.<événement>[N]: matcher is only for preToolCall, postToolCall, preAsk and postAsk |
un matcher sur preGeneration ou postGeneration |
hooks.<événement>[N]: matcher: error parsing regexp: … |
un matcher qui ne compile pas |
hooks.<événement>[N]: timeout must be >= 0 |
un timeout négatif |
hooks.<événement>[N]: onError must be "fail", "warn" or "ignore" |
toute autre valeur d'onError |
Voir aussi
- Protéger l'agent avec des hooks
- Les hooks — explication
- Contrôler les relais vers d'autres agents avec des hooks
- Les démos :
demos/hooks/(quatre hooks Node.js),demos/hooks-ask/(preAsketpostAskautour d'un agent expert)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
|