How to control the hand-offs to other agents with hooks
This guide shows how an agent that asks other agents (ask_agent, /ask-<name>, mm --ask) can check, rewrite or refuse each question before it leaves, and check or replace each answer when it comes back. It assumes you have an agent with at least one entry in mmConfigs (see Let the agent call other agents), and that you know how to write a hook (see Guard the agent with hooks). The examples use Node.js.
The hooks go in the asking agent's file. The asked agent does not need to change, or even to know.
Steps
1. Pick the event
| You want to… | Event | Reply |
|---|---|---|
| refuse a hand-off (a budget, an agent that must not be asked from here) | preAsk |
{"action":"deny","reason":"…"} |
mask secrets, or cut the conversation sent with with_context |
preAsk |
{"prompt":"…","transcript":"…"} |
| give the asked agent standing rules (language, length, format) | preAsk |
{"context":"…"} |
| turn a failure into something the asking model can act on | postAsk |
{"answer":"…"} |
| check the answer's format, add a note for the asking model | postAsk |
{"context":"…"} |
| log every hand-off | postAsk |
nothing |
2. Write the hook
This preAsk hook adds standing rules to every question sent to the agent expert:
// hooks/house-rules.js
const input = JSON.parse(require("fs").readFileSync(0, "utf8"));
const rules = ["Answer in the language of the question.", "At most 120 words."];
// Through ask_agent, the answer is read by the asking model, not a person.
if (input.ask.via === "tool") rules.push("No greetings, no questions back.");
console.log(JSON.stringify({ context: rules.join("\n") }));
And this postAsk hook logs the hand-off and, when the asked agent failed, replaces the error with an instruction for the asking model:
// hooks/after-ask.js
const fs = require("fs");
const path = require("path");
const input = JSON.parse(fs.readFileSync(0, "utf8"));
const line = { agent: input.ask.agent, via: input.ask.via, prompt: input.ask.prompt, durationMs: input.durationMs, error: input.error };
fs.mkdirSync(path.join(input.cwd, ".mm"), { recursive: true });
fs.appendFileSync(path.join(input.cwd, ".mm", "ask-audit.jsonl"), JSON.stringify(line) + "\n");
if (input.error && !(input.answer || "").trim()) {
console.log(JSON.stringify({ answer: `The ${input.ask.agent} agent is unavailable. Tell the user in one sentence; do not answer yourself.` }));
}
3. Try them without mm
echo '{"event":"preAsk","cwd":"/tmp","ask":{"agent":"expert","via":"tool","prompt":"What is an inode?"}}' \
| node hooks/house-rules.js
You should see:
{"context":"Answer in the language of the question.\nAt most 120 words.\nNo greetings, no questions back."}
4. Declare them in the asking agent's file
mmConfigs:
expert:
path: agent.expert.yaml
description: Technical expert. For every technical question.
hooks:
preAsk:
- matcher: ^expert$
command: node "$MM_CONFIG_DIR/hooks/house-rules.js"
postAsk:
- command: node "$MM_CONFIG_DIR/hooks/after-ask.js"
matcher is a regular expression on the asked agent's name; without it, the hook runs for every agent.
5. Start the agent and check
mm agent.yaml
The banner ends with hooks: preAsk×1 postAsk×1. Ask a question the agent hands off, then look at .mm/ask-audit.jsonl: one line per hand-off, with "via":"tool". Type /ask-expert <question> and the line says "via":"command".
Variants
- A budget per question. Declare the same script on
preGeneration(reset a counter file) and onpreAsk(count, and answerdenypast the limit). Count only"via":"tool": a hand-off the user typed, or a script ran, is not the model's doing. - Mask secrets. On
preAsk, rewriteinput.ask.promptandinput.ask.transcriptand print both."transcript":""sends no conversation at all. - A guard. Set
onError: failon apreAskhook that must not be skipped: when it cannot run, the hand-off is denied. - With
yolo: false, the user approves theask_agentcall first;preAskruns after that approval, on the question the model wrote. mm --askfrom a skill. The hooks of the file it reads run too ("via":"cli"). With apostAskhook, stdout carries the answer once the hooks have run, so the calling script reads the replacement.- The asked agent's own hooks. It runs the hooks of its own file, which see
"asked":{"depth":1}. Use them for what belongs to that agent whoever asks it; usepreAsk/postAskfor what belongs to this hand-off.
See also
- Every field and effect: Hooks reference
- Why these hooks belong to the asking agent: Hooks — explanation
- A complete example:
demos/hooks-ask/
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 |
|