bots-garden/mini-mepublic Fork
⑂ feature/hooks
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. 46087c3 · on feature/hooks · k33g · 8d ago
hooks.md · 206 lines · 10.8 KBmarkdown
Blame HistoryOpen raw

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

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

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

### Example

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

## 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](#input)), then end of file |
| stdout | empty, or one JSON object ([Reply](#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

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

```json
{"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 \`<command>\`: <failure>`: 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](../how-to/guard-the-agent-with-hooks.md)
- [Hooks — explanation](../explanation/hooks.md)
- The demo: `demos/hooks/` (four Node.js hooks)