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
-
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", }) } } -
Build it as a WASI module:
tinygo build -o hooks/no_force_push.wasm -target=wasip1 -no-debug ./hooks-src/no_force_push -
Declare it with
wasm:in place ofcommand:;matcher,timeoutandonErrorwork as for a script:hooks: preToolCall: - matcher: ^bash$ wasm: ./hooks/no_force_push.wasm onError: failThe module path is relative to the agent file.
-
Start the agent with
displayHooks: truein the file, and ask for something that runsbash. 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
eventin the input, or theMM_HOOK_EVENTvariable — 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
bashguard and an audit log, the hooks ofdemos/hooksrewritten 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 |
|