bots-garden/mini-mepublic Fork
⑂ feature/last-question
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. f49e056 · on feature/last-question · k33g · 4d ago
hooks.md · 291 lines · 20.7 KBmarkdown
Blame HistoryOpen raw

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

  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
# 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

```yaml
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](#échecs)). |

### Exemple

```yaml
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](#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](#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](cli.md#variables-denvironnement).

## 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`](#lobjet-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

```json
{"event":"preGeneration","cwd":"/home/moi/projet","model":"llmman/hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M","prompt":"list the files"}
```

```json
{"event":"preToolCall","cwd":"/home/moi/projet","model":"llmman/…","tool":{"id":"call_1","name":"bash","input":{"command":"rm -rf build"}}}
```

```json
{"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 \`<command>\`: <é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 avec `mm --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 :

```text
🪝 <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

```text
🤖 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](../how-to/guard-the-agent-with-hooks.md)
- [Les hooks — explication](../explanation/hooks.md)
- [Contrôler les relais vers d'autres agents avec des hooks](../how-to/control-agent-calls-with-hooks.md)
- Les démos : `demos/hooks/` (quatre hooks Node.js), `demos/hooks-ask/` (`preAsk` et `postAsk` autour d'un agent expert)