Reference: hooks
The
hooks:block of the configuration file: the events, the JSON a hook reads and writes, the environment it runs in, and what each decision does to the turn.
Configuration
hooks:
preGeneration: [<hook>, …]
preToolCall: [<hook>, …]
postToolCall: [<hook>, …]
postGeneration: [<hook>, …]
Each event takes a list; the hooks of one event run in the order they are written. An event that is absent, or an empty list, runs nothing. No hooks: block = no hook.
Hook entry
| Key | Type | Default | Description |
|---|---|---|---|
command |
string | — (required) | Run with sh -c "<command>". Must not be empty or blank. |
matcher |
string (regular expression, Go syntax) | "" (every tool) |
preToolCall and postToolCall only. The hook runs for the tools whose name matches. Unanchored: bash also matches my_bash; ^bash$ matches bash alone. |
timeout |
duration | 10s |
How long one run may last before it is killed. 0 = the default. Must be ≥ 0. |
onError |
fail | warn | ignore |
warn |
What a failing run does (see Failures). |
Example
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
Events
| Event | Runs | Tools concerned | Can |
|---|---|---|---|
preGeneration |
once per question, before the first call to the model | — | rewrite the question, add context, stop the turn |
preToolCall |
before each tool call, before the tool's own consent prompt | built-in and MCP, filtered by matcher |
deny the call, replace its arguments |
postToolCall |
after each tool call that ran | built-in and MCP, filtered by matcher |
add context after the tool's output |
postGeneration |
once the turn is over: answered, failed, cancelled or stopped by a hook | — | observe only |
All four run in every front end: the line mode, the window (-tui), an editor (-acp), -p, and an agent started by /ask, ask_agent or mm --ask (with the hooks of its own configuration file). A preGeneration hook does not run on a context-compression summary.
Execution
| Aspect | Value |
|---|---|
| Command | sh -c "<command>" |
| Working directory | the session's: the directory mm runs in, or the editor session's cwd over ACP |
| Environment | mm's own, plus the two variables below |
| stdin | one JSON object (Input), then end of file |
| stdout | empty, or one JSON object (Reply); at most 1 MiB |
| stderr | quoted (first 300 bytes) in the failure line when the run fails; otherwise ignored |
| Concurrency | hooks of one event run one after the other; preToolCall/postToolCall may run concurrently for two tool calls of one model reply |
Environment variables
| Variable | Value |
|---|---|
MM_HOOK_EVENT |
the event: preGeneration, preToolCall, postToolCall or postGeneration |
MM_CONFIG_DIR |
absolute directory of the configuration file that declared the hook |
Input
The JSON object written on the hook's stdin.
| Field | Type | Events | Description |
|---|---|---|---|
event |
string | all | the event's name |
cwd |
string | all | the session's working directory, absolute |
model |
string | all | the model reference, <provider>/<model> |
prompt |
string | preGeneration, postGeneration |
the question as typed — @path and $skill lines included. In a chain, the question as the previous hook left it. In postGeneration: the question the model read, or the marker [prompt blocked by a hook: <reason>] when a hook stopped the turn. |
tool |
object | preToolCall, postToolCall |
the tool call: id (the call's reference, may be empty), name, input (the arguments, as the model sent them or as the previous hook replaced them) |
output |
string | postToolCall |
the tool's output, as the model will read it (JSON when the tool returns something other than text) |
answer |
string | postGeneration |
the text of the last answer; empty when there is none |
error |
string | postGeneration |
the turn's error; absent when the turn succeeded |
Empty fields are omitted.
Examples
{"event":"preGeneration","cwd":"/home/me/project","model":"llmman/hf.co/unsloth/gemma-4-E4B-it-GGUF:Q4_K_M","prompt":"list the files"}
{"event":"preToolCall","cwd":"/home/me/project","model":"llmman/…","tool":{"id":"call_1","name":"bash","input":{"command":"rm -rf build"}}}
Reply
The JSON object a hook may write on stdout. An empty stdout (or whitespace only) is {"action":"continue"}. Every field is optional.
| Field | Type | Read on | Description |
|---|---|---|---|
action |
string | all | continue (default, also when absent), stop, deny or error — see below |
prompt |
string | preGeneration |
replaces the question. Absent = unchanged; "" = an empty question. |
context |
string | preGeneration, postToolCall |
text for the model. preGeneration: appended to the question. postToolCall: appended to the tool's output. Each one is introduced by the line [context added by a hook]. Blank = ignored. |
input |
any JSON value | preToolCall |
replaces the tool call's arguments. Absent = unchanged. |
reason |
string | preGeneration (stop), preToolCall (deny) |
why. |
response |
string | preGeneration (stop) |
what the user reads instead of an answer. Absent or blank: ⛔ This request was blocked by a hook: <reason>. |
error |
string | all (error) |
the hook's own failure message. |
Fields read on another event are ignored.
Actions
| Action | Allowed on | Effect |
|---|---|---|
continue |
all | the turn goes on, with this hook's prompt, context and input applied |
stop |
preGeneration |
the turn ends without calling the model; the following hooks of the event do not run |
deny |
preToolCall |
the tool does not run; the following hooks of the event do not run |
error |
all | a failure, handled according to onError |
anything else, or stop/deny on another event |
— | a failure: action "<action>" is not valid for <event> |
Effects on the turn
preGeneration
| Decision | The model reads | The history keeps | The user sees |
|---|---|---|---|
| no change | the question | the question | — |
prompt and/or context |
the rewritten question, then each context | the rewritten question (the front end's own copy is not changed during the turn) | — |
stop |
nothing: the model is not called | [prompt blocked by a hook: <reason>] (or [prompt blocked by a hook]) followed by the response as the answer |
the response, as the answer |
A chain: each hook reads the prompt the previous one left; contexts accumulate in hook order.
preToolCall
| Decision | The tool | The model reads | The user sees |
|---|---|---|---|
| no change | runs with the model's arguments | the output | the usual tool line |
input |
runs with the replaced arguments | the output | the usual tool line (with the replaced arguments) |
deny |
does not run, and does not ask for consent | [denied by a hook: <reason>] as the tool's output |
line mode: 🪝 <tool> [denied by a hook: <reason>]; window and editor: a failed tool call titled <tool> — denied by a hook |
The history keeps the tool call as the model wrote it, even when input replaced its arguments.
postToolCall
context from every matching hook is joined and appended to the output: <output>\n\n[context added by a hook]\n<context>. The tool's output shown to the user is unchanged.
postGeneration
No effect on the turn. The reply is decoded (an invalid one is a failure) and otherwise ignored.
Failures
A run fails when:
| Failure | Report |
|---|---|
| the command exits with a non-zero status | exit status <n>: <stderr> |
it runs longer than timeout |
timed out after <timeout> |
the turn is cancelled while it runs (preGeneration, preToolCall, postToolCall) |
cancelled |
| stdout is larger than 1 MiB | stdout larger than 1048576 bytes |
| stdout is not JSON | invalid JSON on stdout: … |
action is error |
the reply's error, or the hook reported an error |
action is unknown or not allowed on the event |
action "<action>" is not valid for <event> |
postGeneration is not cancelled with the turn: it runs after a cancellation too (Escape, Ctrl-C, /abort), bounded by its timeout.
What follows depends on onError:
onError |
preGeneration |
preToolCall |
postToolCall, postGeneration |
|---|---|---|---|
fail |
report line, turn stopped (response: ⛔ This request was blocked by a hook: hook failed: …) |
report line, call denied (reason: hook failed: …) |
report line, carry on |
warn |
report line, next hook | report line, next hook | report line, carry on |
ignore |
next hook, silently | next hook, silently | carry on, silently |
The report line is 🪝 <event> hook \`: `: on stdout in the line mode, as a dim line of the transcript in the window, on stderr (the editor's agent logs) over ACP.
Start-up banner
When at least one hook is declared, the banner ends with | hooks: <event>×<count> …, one entry per event that has hooks, in the order preGeneration, preToolCall, postToolCall, postGeneration:
mm v0.4.0 | config: ../agent.yaml | … | yolo: on | hooks: preGeneration×2 preToolCall×1 postToolCall×1 postGeneration×1
Validation errors
Reported at start-up as [config error: <path>: <message>], exit code 1.
| Message | Cause |
|---|---|
hooks: unknown event "<name>" (want one of preGeneration, preToolCall, postToolCall, postGeneration) |
a key of hooks: that is not an event (a typo: preToolcall) |
hooks.<event>[N]: command must not be empty |
no command, or a blank one |
hooks.<event>[N]: matcher is only for preToolCall and postToolCall |
a matcher on preGeneration or postGeneration |
hooks.<event>[N]: matcher: error parsing regexp: … |
a matcher that does not compile |
hooks.<event>[N]: timeout must be >= 0 |
a negative timeout |
hooks.<event>[N]: onError must be "fail", "warn" or "ignore" |
any other onError |
See also
- Guard the agent with hooks
- Hooks — explanation
- The demo:
demos/hooks/(four Node.js 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 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 |
|