Protéger l'agent avec des hooks
Ce guide montre comment lancer votre propre commande à des moments précis d'un tour : pour refuser un appel d'outil dangereux, arrêter une question avant que le modèle la voie, ajouter du contexte, ou journaliser ce que fait l'agent. Il suppose que vous avez un agent qui fonctionne, et un langage dans lequel écrire un petit script. Les exemples sont en Node.js ; tout programme qui lit stdin et écrit sur stdout convient.
Étapes
1. Choisir l'événement
| Vous voulez… | Événement |
|---|---|
| refuser certains appels d'outil, ou changer leurs arguments | preToolCall |
| arrêter ou réécrire une question, ou lui ajouter du contexte | preGeneration |
| ajouter une note après la sortie d'un outil | postToolCall |
| journaliser ou notifier, sans rien changer | postToolCall, postGeneration |
2. Écrire le hook
Le hook lit un objet JSON sur stdin et, quand il a une décision, en écrit un sur stdout. Ne rien écrire laisse le tour continuer. Celui-ci refuse git push --force :
// hooks/no-force-push.js
const input = JSON.parse(require("fs").readFileSync(0, "utf8"));
const command = input.tool.input.command || "";
if (/\bgit\s+push\b.*(--force|-f\b)/.test(command)) {
console.log(JSON.stringify({ action: "deny", reason: "force-push is not allowed in this project" }));
}
La raison est ce que le modèle lit à la place de la sortie de la commande : écrivez-la pour le modèle, afin qu'il puisse prévenir l'utilisateur ou essayer autre chose.
3. L'essayer sans mm
Donnez-lui l'entrée que mm enverrait :
echo '{"event":"preToolCall","cwd":"/tmp","tool":{"name":"bash","input":{"command":"git push -f origin main"}}}' \
| node hooks/no-force-push.js
Vous devez voir :
{"action":"deny","reason":"force-push is not allowed in this project"}
Et rien du tout pour git push origin main.
4. Le déclarer
Dans le fichier YAML de l'agent, à côté du script :
hooks:
preToolCall:
- matcher: ^bash$
command: node "$MM_CONFIG_DIR/hooks/no-force-push.js"
onError: fail
matcherlimite le hook à l'outilbash; sans lui, le hook s'exécute pour chaque appel d'outil.$MM_CONFIG_DIRest le dossier du fichier YAML : le script est trouvé quel que soit le dossier d'oùmmest lancé.onError: failrefuse l'appel quand le hook lui-même échoue (Node absent, erreur de syntaxe, délai dépassé). Utilisez-le pour les gardes ; gardez la valeur par défaut,warn, pour les hooks dont l'échec ne doit pas gêner.
5. Lancer l'agent et vérifier
mm agent.yaml
La bannière se termine par les hooks chargés :
… | yolo: on | hooks: preToolCall×1
Demandez quelque chose qui déclenche la règle (force-push the main branch). Le mode ligne affiche :
🪝 bash [denied by a hook: force-push is not allowed in this project]
et la réponse du modèle explique le refus.
Variantes
- Arrêter une question. Sur
preGeneration, lisezinput.promptet écrivez{"action":"stop","reason":"…","response":"…"}. L'utilisateur litresponse; le modèle n'est pas appelé, et la question est remplacée dans l'historique par[prompt blocked by a hook: <raison>]. - Ajouter du contexte. Sur
preGeneration, écrivez{"context":"…"}: le texte est ajouté après la question, sous[context added by a hook]. SurpostToolCall, le même champ est ajouté après la sortie de l'outil. - Réécrire.
{"prompt":"…"}surpreGenerationremplace la question ;{"input":{…}}surpreToolCallremplace les arguments de l'outil — l'objet entier, donc recopiez les champs que vous gardez. - Journaliser. Sur
postToolCalletpostGeneration, ajoutezinputà un fichier et n'écrivez rien ; mettezonError: ignorepour qu'un disque plein n'interrompe jamais une session. Un même script peut servir plusieurs événements : lisezinput.eventou$MM_HOOK_EVENT. - Un autre langage.
commandest exécutée parsh -c:python3 "$MM_CONFIG_DIR/hooks/guard.py","$MM_CONFIG_DIR/hooks/guard.sh"(exécutable), ou une commandejqen ligne fonctionnent tous. - Plusieurs hooks sur un événement s'exécutent dans l'ordre écrit ; chacun voit la réécriture du précédent, et le premier
stopoudenyarrête la chaîne. - Avec
yolo: false, un hookpreToolCalls'exécute avant la demande d'autorisation : un appel refusé n'est jamais soumis à l'utilisateur. - Dans un éditeur (
mm -acp), les hooks s'exécutent dans le dossier de travail de la session, et un appel refusé apparaît comme un appel d'outil en échec intitulé<outil> — denied by a hook.
Voir aussi
- Chaque champ, action, échec et limite : Référence des hooks
- Pourquoi les hooks sont des commandes externes, et où ils se branchent : Les hooks — explication
- Un exemple complet avec quatre hooks :
demos/hooks/
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 |
|