bots-garden/mini-mepublic Fork
⑂ feature/improve-init
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. fd5d2a7 · on feature/improve-init · k33g · yesterday
guard-the-agent-with-hooks.md · 102 lines · 4.9 KBmarkdown
Blame HistoryOpen raw

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
  • matcher limits the hook to the bash tool; without it, the hook runs for every tool call.
  • $MM_CONFIG_DIR is the directory of the YAML file, so the script is found whatever directory mm is started from.
  • onError: fail denies 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, read input.prompt and print {"action":"stop","reason":"…","response":"…"}. The user reads response; 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]. On postToolCall, the same field is appended to the tool's output.
  • Rewrite. {"prompt":"…"} on preGeneration replaces the question; {"input":{…}} on preToolCall replaces the tool's arguments — the whole object, so copy the fields you keep.
  • Log. On postToolCall and postGeneration, append input to a file and print nothing; set onError: ignore so a full disk never interrupts a session. One script can serve several events: read input.event or $MM_HOOK_EVENT.
  • Another language. command is run by sh -c: python3 "$MM_CONFIG_DIR/hooks/guard.py", "$MM_CONFIG_DIR/hooks/guard.sh" (executable), or an inline jq pipeline all work.
  • A hook without your rights, nothing to install. Write it as a WebAssembly module and declare it with wasm: instead of command:: see How to write a hook as a WebAssembly module.
  • Several hooks on one event run in the order written; each one sees the previous one's rewrite, and the first stop or deny ends the chain.
  • With yolo: false, a preToolCall hook runs before the consent question: a denied call is never put to the user.
  • See which hooks ran. Set displayHooks: true: after each answer, one line per hook run says its event, its command and what it decided — context (178 chars), deny, failed… In an editor, each run is a tool call. See hooks › Display.
  • 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
101
102
# 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`:

```js
// 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:

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

```yaml
hooks:
  preToolCall:
    - matcher: ^bash$
      command: node "$MM_CONFIG_DIR/hooks/no-force-push.js"
      onError: fail
```

- `matcher` limits the hook to the `bash` tool; without it, the hook runs for every tool call.
- `$MM_CONFIG_DIR` is the directory of the YAML file, so the script is found whatever directory `mm` is started from.
- `onError: fail` denies 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

```bash
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`, read `input.prompt` and print `{"action":"stop","reason":"…","response":"…"}`. The user reads `response`; 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]`. On `postToolCall`, the same field is appended to the tool's output.
- **Rewrite.** `{"prompt":"…"}` on `preGeneration` replaces the question; `{"input":{…}}` on `preToolCall` replaces the tool's arguments — the whole object, so copy the fields you keep.
- **Log.** On `postToolCall` and `postGeneration`, append `input` to a file and print nothing; set `onError: ignore` so a full disk never interrupts a session. One script can serve several events: read `input.event` or `$MM_HOOK_EVENT`.
- **Another language.** `command` is run by `sh -c`: `python3 "$MM_CONFIG_DIR/hooks/guard.py"`, `"$MM_CONFIG_DIR/hooks/guard.sh"` (executable), or an inline `jq` pipeline all work.
- **A hook without your rights, nothing to install.** Write it as a WebAssembly module and declare it with `wasm:` instead of `command:`: see [How to write a hook as a WebAssembly module](write-a-wasm-hook.md).
- **Several hooks on one event** run in the order written; each one sees the previous one's rewrite, and the first `stop` or `deny` ends the chain.
- **With `yolo: false`**, a `preToolCall` hook runs before the consent question: a denied call is never put to the user.
- **See which hooks ran.** Set `displayHooks: true`: after each answer, one line per hook run says its event, its command and what it decided — `context (178 chars)`, `deny`, `failed`… In an editor, each run is a tool call. See [hooks › Display](../reference/hooks.md#display).
- **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](../reference/hooks.md)
- Why hooks are external commands, and where they plug in: [Hooks — explanation](../explanation/hooks.md)
- A complete example with four hooks: `demos/hooks/`