How to guard the agent with hooks
This guide shows how to run your own command at fixed moments of a turn: to refuse a dangerous tool call, to stop a question before the model sees it, to add context, or to log what the agent does. It assumes you have an agent that runs, and a language you can write a small script in. The examples use Node.js; any program that reads stdin and writes stdout works.
Steps
1. Pick the event
| You want to… | Event |
|---|---|
| refuse some tool calls, or change their arguments | preToolCall |
| stop or rewrite a question, or add context to it | preGeneration |
| add a note after a tool's output | postToolCall |
| log or notify, without changing anything | postToolCall, postGeneration |
2. Write the hook
The hook reads one JSON object on stdin and, when it has a decision, prints one on stdout. Printing nothing lets the turn go on. This one refuses 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" }));
}
The reason is what the model reads instead of the command's output: write it for the model, so it can tell the user or try something else.
3. Try it without mm
Feed it the input mm would send:
echo '{"event":"preToolCall","cwd":"/tmp","tool":{"name":"bash","input":{"command":"git push -f origin main"}}}' \
| node hooks/no-force-push.js
You should see:
{"action":"deny","reason":"force-push is not allowed in this project"}
And nothing at all for git push origin main.
4. Declare it
In the agent's YAML file, next to the script:
hooks:
preToolCall:
- matcher: ^bash$
command: node "$MM_CONFIG_DIR/hooks/no-force-push.js"
onError: fail
matcherlimits the hook to thebashtool; without it, the hook runs for every tool call.$MM_CONFIG_DIRis the directory of the YAML file, so the script is found whatever directorymmis started from.onError: faildenies the call when the hook itself fails (Node missing, a syntax error, a timeout). Use it for guards; keep the default,warn, for hooks whose failure must not get in the way.
5. Start the agent and check
mm agent.yaml
The banner ends with the hooks it loaded:
… | yolo: on | hooks: preToolCall×1
Ask for something that triggers the rule (force-push the main branch). The line mode shows:
🪝 bash [denied by a hook: force-push is not allowed in this project]
and the model's answer explains the refusal.
Variants
- Stop a question. On
preGeneration, readinput.promptand print{"action":"stop","reason":"…","response":"…"}. The user readsresponse; the model is not called, and the question is replaced in the history by[prompt blocked by a hook: <reason>]. - Add context. On
preGeneration, print{"context":"…"}: the text is appended to the question under[context added by a hook]. OnpostToolCall, the same field is appended to the tool's output. - Rewrite.
{"prompt":"…"}onpreGenerationreplaces the question;{"input":{…}}onpreToolCallreplaces the tool's arguments — the whole object, so copy the fields you keep. - Log. On
postToolCallandpostGeneration, appendinputto a file and print nothing; setonError: ignoreso a full disk never interrupts a session. One script can serve several events: readinput.eventor$MM_HOOK_EVENT. - Another language.
commandis run bysh -c:python3 "$MM_CONFIG_DIR/hooks/guard.py","$MM_CONFIG_DIR/hooks/guard.sh"(executable), or an inlinejqpipeline all work. - Several hooks on one event run in the order written; each one sees the previous one's rewrite, and the first
stopordenyends the chain. - With
yolo: false, apreToolCallhook runs before the consent question: a denied call is never put to the user. - In an editor (
mm -acp), the hooks run in the session's working directory, and a denied call shows as a failed tool call titled<tool>: <command> — denied by a hook.
See also
- Every field, action, failure and limit: Hooks reference
- Why hooks are external commands, and where they plug in: Hooks — explanation
- A complete example with four 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 |
|