bots-garden/mini-mepublic Fork
⑂ v0.9.0
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 v0.9.0 · k33g · yesterday
write-a-wasm-hook.md · 77 lines · 3.2 KBmarkdown
Blame HistoryOpen raw

How to write a hook as a WebAssembly module

This guide shows how to write a hook that runs as a WebAssembly module inside mm, in a sandbox, instead of a script: nothing to install beside mm, no process started per event, and a hook that sees no file, no environment variable and no network except what its entry opens. It assumes you know how hooks work and that TinyGo is installed.

Steps

  1. Write the hook as an ordinary Go program. It reads the same JSON object on stdin as a script hook, and prints the same reply on stdout — or nothing, to let the turn go on. This one refuses git push --force:

    // hooks-src/no_force_push/main.go
    package main
    
    import (
    	"encoding/json"
    	"os"
    	"regexp"
    )
    
    var forcePush = regexp.MustCompile(`\bgit\s+push\b.*(--force|-f\b)`)
    
    func main() {
    	var in struct {
    		Tool struct {
    			Input struct {
    				Command string `json:"command"`
    			} `json:"input"`
    		} `json:"tool"`
    	}
    	if err := json.NewDecoder(os.Stdin).Decode(&in); err != nil {
    		os.Exit(2)
    	}
    	if forcePush.MatchString(in.Tool.Input.Command) {
    		json.NewEncoder(os.Stdout).Encode(map[string]string{
    			"action": "deny",
    			"reason": "force-push is not allowed in this project",
    		})
    	}
    }
    
  2. Build it as a WASI module:

    tinygo build -o hooks/no_force_push.wasm -target=wasip1 -no-debug ./hooks-src/no_force_push
    
  3. Declare it with wasm: in place of command:; matcher, timeout and onError work as for a script:

    hooks:
      preToolCall:
        - matcher: ^bash$
          wasm: ./hooks/no_force_push.wasm
          onError: fail
    

    The module path is relative to the agent file.

  4. Start the agent with displayHooks: true in the file, and ask for something that runs bash. After the answer, each run of the hook is listed under its module's name:

      🪝 1. preToolCall bash · no_force_push.wasm → deny
    

Variants

  • A hook that reads or writes files. Open the folders it needs, relative to the working directory: read: [rules] (read-only), write: [logs] (read-write). It opens them by the same name (logs/hooks-audit.jsonl); nothing else exists for it. A folder must exist when the hook runs.
  • One module for several events. Read event in the input, or the MM_HOOK_EVENT variable — the only one the module sees.
  • A hook that needs the whole working directory (listing its files, say) or the network stays a script: a wasm hook can open folders inside the working directory, not the directory itself, and has no network.
  • Paths inside the module. Write them clean: logs/a.jsonl, not ./logs/a.jsonl — TinyGo's WASI layer refuses ./ and ...
  • The first run of a process compiles the module (a few hundred milliseconds); the runs after it take well under a millisecond.

See also

  • A complete example — a secret filter, a bash guard and an audit log, the hooks of demos/hooks rewritten as modules: demos/wasm-hooks/
  • Every key, what the module sees, the failures: hooks reference
  • Why, and what was measured: Hooks — explanation
 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
# How to write a hook as a WebAssembly module

This guide shows how to write a hook that runs as a WebAssembly module inside mm, in a sandbox, instead of a script: nothing to install beside mm, no process started per event, and a hook that sees no file, no environment variable and no network except what its entry opens. It assumes you know [how hooks work](guard-the-agent-with-hooks.md) and that [TinyGo](https://tinygo.org) is installed.

## Steps

1. Write the hook as an ordinary Go program. It reads the same JSON object on stdin as a script hook, and prints the same reply on stdout — or nothing, to let the turn go on. This one refuses `git push --force`:

   ```go
   // hooks-src/no_force_push/main.go
   package main

   import (
   	"encoding/json"
   	"os"
   	"regexp"
   )

   var forcePush = regexp.MustCompile(`\bgit\s+push\b.*(--force|-f\b)`)

   func main() {
   	var in struct {
   		Tool struct {
   			Input struct {
   				Command string `json:"command"`
   			} `json:"input"`
   		} `json:"tool"`
   	}
   	if err := json.NewDecoder(os.Stdin).Decode(&in); err != nil {
   		os.Exit(2)
   	}
   	if forcePush.MatchString(in.Tool.Input.Command) {
   		json.NewEncoder(os.Stdout).Encode(map[string]string{
   			"action": "deny",
   			"reason": "force-push is not allowed in this project",
   		})
   	}
   }
   ```

2. Build it as a WASI module:

   ```bash
   tinygo build -o hooks/no_force_push.wasm -target=wasip1 -no-debug ./hooks-src/no_force_push
   ```

3. Declare it with `wasm:` in place of `command:`; `matcher`, `timeout` and `onError` work as for a script:

   ```yaml
   hooks:
     preToolCall:
       - matcher: ^bash$
         wasm: ./hooks/no_force_push.wasm
         onError: fail
   ```

   The module path is relative to the agent file.

4. Start the agent with `displayHooks: true` in the file, and ask for something that runs `bash`. After the answer, each run of the hook is listed under its module's name:

   ```text
     🪝 1. preToolCall bash · no_force_push.wasm → deny
   ```

## Variants

- **A hook that reads or writes files.** Open the folders it needs, relative to the working directory: `read: [rules]` (read-only), `write: [logs]` (read-write). It opens them by the same name (`logs/hooks-audit.jsonl`); nothing else exists for it. A folder must exist when the hook runs.
- **One module for several events.** Read `event` in the input, or the `MM_HOOK_EVENT` variable — the only one the module sees.
- **A hook that needs the whole working directory** (listing its files, say) or the network stays a script: a wasm hook can open folders inside the working directory, not the directory itself, and has no network.
- **Paths inside the module.** Write them clean: `logs/a.jsonl`, not `./logs/a.jsonl` — TinyGo's WASI layer refuses `./` and `..`.
- **The first run of a process** compiles the module (a few hundred milliseconds); the runs after it take well under a millisecond.

## See also

- A complete example — a secret filter, a `bash` guard and an audit log, the hooks of `demos/hooks` rewritten as modules: `demos/wasm-hooks/`
- Every key, what the module sees, the failures: [hooks reference](../reference/hooks.md#wasm-hooks)
- Why, and what was measured: [Hooks — explanation](../explanation/hooks.md#hooks-as-webassembly-modules)