bots-garden/mini-mepublic Fork
⑂ f7f4a05
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.

k33g committed 2026-09-22T23:55:08+02:00 Browse files
f7f4a05 parent: 5f6abb3
modified .tickets/issues/0036-hooks.yaml +517 -3
@@ -6,9 +6,523 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-21T06:32:48.566Z
9-updatedAt: 2026-09-22T21:02:05.404Z
9+updatedAt: 2026-09-22T21:54:11.101Z
1010 labels: []
11-milestone: v0.1.1
12-body: ""
11+milestone: v0.1.2
12+body: |
13+ ## Overview
14+ Design a hook system for mini-me to allow extensibility at key points in the agent lifecycle
15+ and execution flow without modifying core code.
16+
17+ ## Key Requirements (from user feedback)
18+ - Hooks can be written in **any language** (Python, Bash, Node.js, Lua, etc.)
19+ - Hooks communicate via **stdin/stdout** with JSON payloads
20+ - Must be able to:
21+ 1. **Detect** patterns in user message before it reaches LLM
22+ 2. **Modify/Retreat** the user message before sending
23+ 3. **Stop** the turn based on detection/validation results
24+ 4. **Add context** (additional messages, metadata) to the conversation
25+
26+ ## Architecture Design
27+
28+ ### 1. Hook Types (Extension Points)
29+
30+ #### Lifecycle Hooks
31+ - **preStartup**: Before configuration loading (can modify args/env)
32+ - **postConfig**: After configuration loaded, before engine init
33+ - **preShutdown**: Before agent exits
34+ - **onError**: When an error occurs
35+
36+ #### Generation Hooks
37+ - **preGeneration**: Before `engine.Generate()` call - **PRIMARY FOR USER REQUIREMENTS**
38+ - **postGeneration**: After receiving model response
39+ - **onMessage**: When user message is received
40+
41+ #### Tool Hooks
42+ - **preToolCall**: Before executing any tool
43+ - **postToolCall**: After tool execution completes
44+
45+ #### Session Hooks
46+ - **onSessionStart**: When new session begins
47+ - **onSessionEnd**: When session ends
48+
49+ #### Frontend Hooks
50+ - **onTUIOpen/onTUIClose**: TUI window events
51+ - **onACPConnect/onACPDisconnect**: ACP client events
52+ - **onPromptMode**: When running in prompt mode
53+
54+ ### 2. Implementation: stdin/stdout Communication
55+
56+ All hooks execute as external processes communicating via stdin/stdout:
57+
58+ ```
59+ mini-me
60+ │
61+ ├─▶ [STDIN] → JSON payload
62+ │
63+ hook_script (any language)
64+ │
65+ ├─◀ [STDOUT] → JSON result
66+ └─◀ [STDERR] → Hook's own logs (captured separately)
67+ ```
68+
69+ **Pros**: Language agnostic, no recompilation, isolated errors, maximum flexibility
70+ **Cons**: Process overhead, requires JSON, security considerations
71+
72+ ### 3. Communication Protocol
73+
74+ #### Input Format (mini-me → hook via STDIN)
75+ ```json
76+ {
77+ "hook": {
78+ "type": "preGeneration",
79+ "timestamp": "2026-09-22T12:00:00Z",
80+ "timeout": 5
81+ },
82+ "context": {
83+ "session_id": "abc123",
84+ "working_dir": "/home/user/project",
85+ "system_prompt": "Tu es un assistant utile...",
86+ "tools": ["bash", "read_file", "write_file"]
87+ },
88+ "payload": {
89+ "messages": [
90+ {"role": "system", "content": [{"type": "text", "text": "Tu es un assistant..."}]},
91+ {"role": "user", "content": [{"type": "text", "text": "list files"}]}
92+ ],
93+ "current_message": {"role": "user", "content": [{"type": "text", "text": "list files"}]},
94+ "current_text": "list files"
95+ },
96+ "metadata": {
97+ "hook_chain": ["securityHook", "previousHook"],
98+ "previous_hook_result": null
99+ }
100+ }
101+ ```
102+
103+ #### Output Format (hook → mini-me via STDOUT)
104+
105+ **Case 1: Continue with (possibly modified) messages**
106+ ```json
107+ {
108+ "action": "continue",
109+ "messages": [
110+ {"role": "system", "content": [{"type": "text", "text": "System message"}]},
111+ {"role": "user", "content": [{"type": "text", "text": "modified prompt"}]}
112+ ],
113+ "metadata": {"hook_name": "myHook", "processing_time_ms": 15}
114+ }
115+ ```
116+
117+ **Case 2: Stop the turn**
118+ ```json
119+ {
120+ "action": "stop",
121+ "response": "⚠️ This prompt contains a forbidden pattern.",
122+ "reason": "security_violation",
123+ "metadata": {"blocked_pattern": "rm -rf"}
124+ }
125+ ```
126+
127+ **Case 3: Hook execution error**
128+ ```json
129+ {
130+ "action": "error",
131+ "error": "Failed to read context file",
132+ "error_type": "io_error"
133+ }
134+ ```
135+
136+ ### 4. Hook Implementation Examples
137+
138+ All hooks receive JSON via stdin, return JSON via stdout.
139+
140+ #### Python: Full Feature Example (Detect + Retreat + Add Context + Stop)
141+ ```python
142+ #!/usr/bin/env python3
143+ import json, sys, re, os
144+
145+ def main():
146+ try:
147+ input_data = json.load(sys.stdin)
148+ current_text = input_data["payload"]["current_text"]
149+ messages = input_data["payload"]["messages"]
150+ working_dir = input_data["context"]["working_dir"]
151+
152+ # 1. DETECT: Block dangerous patterns
153+ dangerous = ["rm -rf", "; sudo ", "eval ", "curl.*|.*bash"]
154+ for pattern in dangerous:
155+ if re.search(pattern, current_text, re.IGNORECASE):
156+ print(json.dumps({
157+ "action": "stop",
158+ "response": "⚠️ Forbidden pattern detected.",
159+ "reason": "security_blocked",
160+ "metadata": {"pattern": pattern, "hook_name": "security"}
161+ }))
162+ return
163+
164+ # 2. RETREAT: Spell checking
165+ corrected = current_text
166+ replacements = {
167+ r"a la\b": "à la",
168+ r"ces ci\b": "ceci",
169+ r"bcp\b": "beaucoup"
170+ }
171+ for pattern, replacement in replacements.items():
172+ corrected = re.sub(pattern, replacement, corrected, flags=re.IGNORECASE)
173+
174+ # 3. ADD CONTEXT: Inject relevant files
175+ new_messages = messages.copy()
176+ if "main.go" in corrected.lower():
177+ try:
178+ filepath = os.path.join(working_dir, "main.go")
179+ with open(filepath) as f:
180+ new_messages.insert(1, {
181+ "role": "system",
182+ "content": [{"type": "text", "text": f"File: main.go\n```go\n{f.read()}\n```"}]
183+ })
184+ except: pass
185+
186+ # Update user message with corrected text
187+ if corrected != current_text:
188+ new_messages[-1] = {
189+ "role": "user",
190+ "content": [{"type": "text", "text": corrected}]
191+ }
192+
193+ # 4. Return result
194+ print(json.dumps({
195+ "action": "continue",
196+ "messages": new_messages,
197+ "metadata": {
198+ "hook_name": "py_full_hook",
199+ "corrections_made": corrected != current_text,
200+ "context_added": len(new_messages) > len(messages)
201+ }
202+ }))
203+
204+ except json.JSONDecodeError as e:
205+ print(json.dumps({"action": "error", "error": f"Invalid JSON: {str(e)}", "error_type": "parse_error"}))
206+ except Exception as e:
207+ print(json.dumps({"action": "error", "error": str(e), "error_type": "runtime_error"}))
208+
209+ if __name__ == "__main__":
210+ main()
211+ ```
212+
213+ #### Bash: Simple Validation + Stop
214+ ```bash
215+ #!/bin/bash
216+ input=$(cat)
217+
218+ current_text=$(echo "$input" | jq -r '.payload.current_text')
219+ session_id=$(echo "$input" | jq -r '.context.session_id')
220+
221+ # 1. DETECT: Empty message
222+ if [ -z "$current_text" ] || [ "$current_text" = " " ]; then
223+ jq -n \
224+ --arg response "⚠️ Empty message, please provide a prompt." \
225+ --arg reason "empty_prompt" \
226+ '{action: "stop", response: $response, reason: $reason, metadata: {hook_name: "validator"}}'
227+ exit 0
228+ fi
229+
230+ # 2. VALIDATE: Minimum length
231+ min_length=3
232+ if [ ${#current_text} -lt $min_length ]; then
233+ jq -n \
234+ --arg response "⚠️ Message too short (min $min_length characters)." \
235+ --arg reason "too_short" \
236+ '{action: "stop", response: $response, reason: $reason, metadata: {min_length: '"$min_length"', hook_name: "validator"}}'
237+ exit 0
238+ fi
239+
240+ # 3. Continue with original messages
241+ jq -n --argjson messages "$(echo "$input" | jq '.payload.messages')" '{
242+ action: "continue",
243+ messages: $messages,
244+ metadata: {hook_name: "validator", validated: true}
245+ }'
246+ ```
247+
248+ ### 5. Configuration Schema
249+
250+ ```yaml
251+ hooks:
252+ # Security settings for ALL hooks
253+ security:
254+ allowedPaths:
255+ - /usr/local/bin/mm-hooks
256+ - /home/user/.mm/hooks
257+ - /opt/mm/hooks
258+ allowedExtensions:
259+ - .sh
260+ - .py
261+ - .js
262+ - .lua
263+ - .rb
264+ - "" # Allow scripts without extension
265+ forbiddenExtensions:
266+ - .go # Prevent compiled Go hooks
267+ defaultTimeout: 5s
268+ maxOutputSize: 10240 # 10KB max per hook
269+
270+ # Hook definitions by type
271+ preGeneration:
272+ # Priority 10 = highest (executes first)
273+ - command: /path/to/security.py
274+ priority: 10
275+ mode: sync
276+ onError: fail
277+ timeout: 3s
278+
279+ # Priority 20 = second
280+ - command: /path/to/spellcheck.py
281+ priority: 20
282+ mode: sync
283+ onError: warn
284+ timeout: 2s
285+
286+ # Priority 30 = third
287+ - command: /path/to/context-injector.sh
288+ priority: 30
289+ mode: sync
290+ onError: warn
291+ timeout: 3s
292+
293+ # Priority 40 = fourth
294+ - command: /path/to/validator.py
295+ priority: 40
296+ mode: sync
297+ onError: fail
298+ timeout: 2s
299+
300+ # Priority 100 = last (async, non-blocking)
301+ - command: /path/to/logger.sh
302+ priority: 100
303+ mode: async
304+ onError: ignore
305+ timeout: 5s
306+ ```
307+
308+ **Priority System**: Lower number = higher priority = executes first
309+
310+ ### 6. Error Handling and Execution Modes
311+
312+ | Mode | Blocking | Error Behavior | Use Case |
313+ |------|----------|----------------|----------|
314+ | `sync` | Yes | According to `onError` (fail/warn/ignore) | Critical hooks |
315+ | `async` | No | Always log, never block | Non-critical hooks |
316+
317+ **onError values:**
318+ - `fail`: Stop execution, return error to user
319+ - `warn`: Log warning, continue execution
320+ - `ignore`: Silent continue
321+
322+ **Exit codes from hooks:**
323+ - `0`: Success
324+ - `1`: JSON parse error
325+ - `2`: Runtime error
326+ - `143`: Timeout (SIGTERM)
327+
328+ ### 7. Execution Flow
329+
330+ ```
331+ User Input: "delete all files"
332+ │
333+ ▼
334+ ┌─────────────────────────────────────────┐
335+ │ mini-me: Build JSON payload │
336+ │ {type: "preGeneration", current_text: ...}│
337+ └───────────────────────┬─────────────────┘
338+ │
339+ ▼
340+ ┌─────────────────────────────────────────┐
341+ │ mini-me: Execute hook #1 (security.py) │
342+ │ ├─ stdin: JSON payload │
343+ │ └─ stdout: JSON result │
344+ └───────────────────────┬─────────────────┘
345+ │
346+ ▼
347+ ┌─────────────────────────────────────────┐
348+ │ Hook 1 checks for patterns │
349+ │ if "rm -rf" found: │
350+ │ return {"action": "stop", ...} │
351+ │ else: │
352+ │ return {"action": "continue", ...} │
353+ └───────────────────────┬─────────────────┘
354+ │
355+ ┌───────────────┴───────────────┐
356+ ▼ ▼
357+ ┌─────────────────┐ ┌─────────────────┐
358+ │ Hook #2 │ │ STOP TURN │
359+ │ (spellcheck) │ │ Return response │
360+ └─────────────────┘ └─────────────────┘
361+ │
362+ ▼
363+ ┌─────────────────────────┐
364+ │ Hook #3 (context) │
365+ └───────────────────┬───────┘
366+ │
367+ ▼
368+ ┌───────────────────┐
369+ │ Hook #4 (validate)│
370+ └───────────────────┘
371+ │
372+ ▼
373+ ┌─────────────────────────────────────────┐
374+ │ engine.Generate() with final messages │
375+ │ (potentially modified by hooks) │
376+ └─────────────────────────────────────────┘
377+ ```
378+
379+ ### 8. Security Considerations
380+
381+ | Risk | Mitigation |
382+ |------|------------|
383+ | Malicious hook script | Allowlist of executable paths, allowed extensions |
384+ | Blocking hook (sync mode) | Configurable timeout (default: 5s) |
385+ | Resource exhaustion | Max output size limit (default: 10KB) |
386+ | Hook crashes | Process isolation, stderr captured separately |
387+ | Sensitive data in hooks | Hooks receive only configured payload fields |
388+
389+ ### 9. Implementation Sketch (Go)
390+
391+ ```go
392+ // internal/hooks/runner.go
393+
394+ type HookResult struct {
395+ Action string `json:"action"` // continue, stop, error
396+ Messages []*ai.Message `json:"messages,omitempty"`
397+ Response string `json:"response,omitempty"`
398+ Reason string `json:"reason,omitempty"`
399+ Error string `json:"error,omitempty"`
400+ ErrorType string `json:"error_type,omitempty"`
401+ Metadata map[string]interface{} `json:"metadata,omitempty"`
402+ }
403+
404+ type HookStopResult struct {
405+ Response string
406+ Reason string
407+ }
408+
409+ func (r *HookRunner) RunPreGenerationHooks(
410+ ctx context.Context,
411+ messages []*ai.Message,
412+ system string,
413+ tools []ai.ToolRef,
414+ sessionID string,
415+ workingDir string,
416+ ) ([]*ai.Message, *HookStopResult, error) {
417+
418+ payload := PreGenerationPayload{
419+ Hook: HookInfo{Type: "preGeneration", Timestamp: time.Now()},
420+ Context: HookContext{
421+ SessionID: sessionID,
422+ WorkingDir: workingDir,
423+ System: system,
424+ Tools: tools,
425+ },
426+ Payload: PreGenerationPayloadData{
427+ Messages: messages,
428+ CurrentMessage: messages[len(messages)-1],
429+ CurrentText: messages[len(messages)-1].Content[0].Text,
430+ },
431+ }
432+
433+ jsonInput, _ := json.Marshal(payload)
434+ hooks := r.getSortedHooks(HookTypePreGeneration)
435+ var finalMessages = messages
436+
437+ for _, hook := range hooks {
438+ result, err := r.runExternalHook(ctx, hook, jsonInput)
439+ if err != nil {
440+ if hook.Mode == "sync" && hook.OnError == "fail" {
441+ return nil, nil, err
442+ }
443+ log.Printf("[hooks] %s error: %v", hook.Command, err)
444+ continue
445+ }
446+
447+ switch result.Action {
448+ case "continue":
449+ finalMessages = result.Messages
450+ case "stop":
451+ return nil, &HookStopResult{
452+ Response: result.Response,
453+ Reason: result.Reason,
454+ }, nil
455+ case "error":
456+ if hook.Mode == "sync" && hook.OnError == "fail" {
457+ return nil, nil, fmt.Errorf("hook error: %s", result.Error)
458+ }
459+ }
460+ }
461+
462+ return finalMessages, nil, nil
463+ }
464+
465+ func (r *HookRunner) runExternalHook(
466+ ctx context.Context,
467+ hook HookConfig,
468+ input []byte,
469+ ) (HookResult, error) {
470+
471+ cmd := exec.CommandContext(ctx, hook.Command)
472+ cmd.Stdin = bytes.NewReader(input)
473+
474+ var stdoutBuf, stderrBuf bytes.Buffer
475+ cmd.Stdout = &stdoutBuf
476+ cmd.Stderr = &stderrBuf
477+
478+ err := cmd.Run()
479+
480+ if err != nil {
481+ return HookResult{}, fmt.Errorf("hook %s failed: %v (stderr: %s)",
482+ hook.Command, err, stderrBuf.String())
483+ }
484+
485+ var result HookResult
486+ if err := json.Unmarshal(stdoutBuf.Bytes(), &result); err != nil {
487+ return HookResult{}, fmt.Errorf("hook %s returned invalid JSON: %v",
488+ hook.Command, err)
489+ }
490+
491+ return result, nil
492+ }
493+ ```
494+
495+ ### 10. Summary: How User Requirements Are Met
496+
497+ | Requirement | Mechanism | Example |
498+ |------------|-----------|---------|
499+ | **Detect** patterns | Analyze `payload.current_text` in preGeneration hook | Block "rm -rf" |
500+ | **Modify/Retreat** message | Return modified `messages` array | Fix spelling, reformat |
501+ | **Add context** | Insert system messages into `messages` | Inject file contents |
502+ | **Stop turn** | Return `{action: "stop", response: "..."}` | Validation failed |
503+
504+ All capabilities work via **stdin/stdout JSON communication** with hooks in **any language**.
505+
506+ ### 11. Open Questions
507+
508+ 1. Should hooks have access to full conversation history (beyond current turn)?
509+ - Current: Only current turn messages
510+ - Option: Add `conversation_history` with configurable depth
511+
512+ 2. Should hooks be able to persist state between calls?
513+ - Current: Stateless
514+ - Option: Designated state directory for hooks
515+
516+ 3. Should hooks be able to execute tools?
517+ - Risk: Security
518+ - Suggestion: No, hooks should only observe/modify data
519+
520+ 4. Should there be a total timeout for all hooks of a type?
521+ - Current: Per-hook timeout
522+ - Option: Add `totalTimeout` for hook chains
523+
524+ 5. Should hooks be able to modify tool configurations?
525+ - Current: Tools are read-only
526+ - Suggestion: No, for security
13527 tasks: []
14528 comments: []
@@ -6,9 +6,523 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-21T06:32:48.566Z8 createdAt: 2026-09-21T06:32:48.566Z
9-updatedAt: 2026-09-22T21:02:05.404Z9+updatedAt: 2026-09-22T21:54:11.101Z
10 labels: []10 labels: []
11-milestone: v0.1.111+milestone: v0.1.2
12-body: ""12+body: |
13+ ## Overview
14+ Design a hook system for mini-me to allow extensibility at key points in the agent lifecycle
15+ and execution flow without modifying core code.
16+
17+ ## Key Requirements (from user feedback)
18+ - Hooks can be written in **any language** (Python, Bash, Node.js, Lua, etc.)
19+ - Hooks communicate via **stdin/stdout** with JSON payloads
20+ - Must be able to:
21+ 1. **Detect** patterns in user message before it reaches LLM
22+ 2. **Modify/Retreat** the user message before sending
23+ 3. **Stop** the turn based on detection/validation results
24+ 4. **Add context** (additional messages, metadata) to the conversation
25+
26+ ## Architecture Design
27+
28+ ### 1. Hook Types (Extension Points)
29+
30+ #### Lifecycle Hooks
31+ - **preStartup**: Before configuration loading (can modify args/env)
32+ - **postConfig**: After configuration loaded, before engine init
33+ - **preShutdown**: Before agent exits
34+ - **onError**: When an error occurs
35+
36+ #### Generation Hooks
37+ - **preGeneration**: Before `engine.Generate()` call - **PRIMARY FOR USER REQUIREMENTS**
38+ - **postGeneration**: After receiving model response
39+ - **onMessage**: When user message is received
40+
41+ #### Tool Hooks
42+ - **preToolCall**: Before executing any tool
43+ - **postToolCall**: After tool execution completes
44+
45+ #### Session Hooks
46+ - **onSessionStart**: When new session begins
47+ - **onSessionEnd**: When session ends
48+
49+ #### Frontend Hooks
50+ - **onTUIOpen/onTUIClose**: TUI window events
51+ - **onACPConnect/onACPDisconnect**: ACP client events
52+ - **onPromptMode**: When running in prompt mode
53+
54+ ### 2. Implementation: stdin/stdout Communication
55+
56+ All hooks execute as external processes communicating via stdin/stdout:
57+
58+ ```
59+ mini-me
60+ │
61+ ├─▶ [STDIN] → JSON payload
62+ │
63+ hook_script (any language)
64+ │
65+ ├─◀ [STDOUT] → JSON result
66+ └─◀ [STDERR] → Hook's own logs (captured separately)
67+ ```
68+
69+ **Pros**: Language agnostic, no recompilation, isolated errors, maximum flexibility
70+ **Cons**: Process overhead, requires JSON, security considerations
71+
72+ ### 3. Communication Protocol
73+
74+ #### Input Format (mini-me → hook via STDIN)
75+ ```json
76+ {
77+ "hook": {
78+ "type": "preGeneration",
79+ "timestamp": "2026-09-22T12:00:00Z",
80+ "timeout": 5
81+ },
82+ "context": {
83+ "session_id": "abc123",
84+ "working_dir": "/home/user/project",
85+ "system_prompt": "Tu es un assistant utile...",
86+ "tools": ["bash", "read_file", "write_file"]
87+ },
88+ "payload": {
89+ "messages": [
90+ {"role": "system", "content": [{"type": "text", "text": "Tu es un assistant..."}]},
91+ {"role": "user", "content": [{"type": "text", "text": "list files"}]}
92+ ],
93+ "current_message": {"role": "user", "content": [{"type": "text", "text": "list files"}]},
94+ "current_text": "list files"
95+ },
96+ "metadata": {
97+ "hook_chain": ["securityHook", "previousHook"],
98+ "previous_hook_result": null
99+ }
100+ }
101+ ```
102+
103+ #### Output Format (hook → mini-me via STDOUT)
104+
105+ **Case 1: Continue with (possibly modified) messages**
106+ ```json
107+ {
108+ "action": "continue",
109+ "messages": [
110+ {"role": "system", "content": [{"type": "text", "text": "System message"}]},
111+ {"role": "user", "content": [{"type": "text", "text": "modified prompt"}]}
112+ ],
113+ "metadata": {"hook_name": "myHook", "processing_time_ms": 15}
114+ }
115+ ```
116+
117+ **Case 2: Stop the turn**
118+ ```json
119+ {
120+ "action": "stop",
121+ "response": "⚠️ This prompt contains a forbidden pattern.",
122+ "reason": "security_violation",
123+ "metadata": {"blocked_pattern": "rm -rf"}
124+ }
125+ ```
126+
127+ **Case 3: Hook execution error**
128+ ```json
129+ {
130+ "action": "error",
131+ "error": "Failed to read context file",
132+ "error_type": "io_error"
133+ }
134+ ```
135+
136+ ### 4. Hook Implementation Examples
137+
138+ All hooks receive JSON via stdin, return JSON via stdout.
139+
140+ #### Python: Full Feature Example (Detect + Retreat + Add Context + Stop)
141+ ```python
142+ #!/usr/bin/env python3
143+ import json, sys, re, os
144+
145+ def main():
146+ try:
147+ input_data = json.load(sys.stdin)
148+ current_text = input_data["payload"]["current_text"]
149+ messages = input_data["payload"]["messages"]
150+ working_dir = input_data["context"]["working_dir"]
151+
152+ # 1. DETECT: Block dangerous patterns
153+ dangerous = ["rm -rf", "; sudo ", "eval ", "curl.*|.*bash"]
154+ for pattern in dangerous:
155+ if re.search(pattern, current_text, re.IGNORECASE):
156+ print(json.dumps({
157+ "action": "stop",
158+ "response": "⚠️ Forbidden pattern detected.",
159+ "reason": "security_blocked",
160+ "metadata": {"pattern": pattern, "hook_name": "security"}
161+ }))
162+ return
163+
164+ # 2. RETREAT: Spell checking
165+ corrected = current_text
166+ replacements = {
167+ r"a la\b": "à la",
168+ r"ces ci\b": "ceci",
169+ r"bcp\b": "beaucoup"
170+ }
171+ for pattern, replacement in replacements.items():
172+ corrected = re.sub(pattern, replacement, corrected, flags=re.IGNORECASE)
173+
174+ # 3. ADD CONTEXT: Inject relevant files
175+ new_messages = messages.copy()
176+ if "main.go" in corrected.lower():
177+ try:
178+ filepath = os.path.join(working_dir, "main.go")
179+ with open(filepath) as f:
180+ new_messages.insert(1, {
181+ "role": "system",
182+ "content": [{"type": "text", "text": f"File: main.go\n```go\n{f.read()}\n```"}]
183+ })
184+ except: pass
185+
186+ # Update user message with corrected text
187+ if corrected != current_text:
188+ new_messages[-1] = {
189+ "role": "user",
190+ "content": [{"type": "text", "text": corrected}]
191+ }
192+
193+ # 4. Return result
194+ print(json.dumps({
195+ "action": "continue",
196+ "messages": new_messages,
197+ "metadata": {
198+ "hook_name": "py_full_hook",
199+ "corrections_made": corrected != current_text,
200+ "context_added": len(new_messages) > len(messages)
201+ }
202+ }))
203+
204+ except json.JSONDecodeError as e:
205+ print(json.dumps({"action": "error", "error": f"Invalid JSON: {str(e)}", "error_type": "parse_error"}))
206+ except Exception as e:
207+ print(json.dumps({"action": "error", "error": str(e), "error_type": "runtime_error"}))
208+
209+ if __name__ == "__main__":
210+ main()
211+ ```
212+
213+ #### Bash: Simple Validation + Stop
214+ ```bash
215+ #!/bin/bash
216+ input=$(cat)
217+
218+ current_text=$(echo "$input" | jq -r '.payload.current_text')
219+ session_id=$(echo "$input" | jq -r '.context.session_id')
220+
221+ # 1. DETECT: Empty message
222+ if [ -z "$current_text" ] || [ "$current_text" = " " ]; then
223+ jq -n \
224+ --arg response "⚠️ Empty message, please provide a prompt." \
225+ --arg reason "empty_prompt" \
226+ '{action: "stop", response: $response, reason: $reason, metadata: {hook_name: "validator"}}'
227+ exit 0
228+ fi
229+
230+ # 2. VALIDATE: Minimum length
231+ min_length=3
232+ if [ ${#current_text} -lt $min_length ]; then
233+ jq -n \
234+ --arg response "⚠️ Message too short (min $min_length characters)." \
235+ --arg reason "too_short" \
236+ '{action: "stop", response: $response, reason: $reason, metadata: {min_length: '"$min_length"', hook_name: "validator"}}'
237+ exit 0
238+ fi
239+
240+ # 3. Continue with original messages
241+ jq -n --argjson messages "$(echo "$input" | jq '.payload.messages')" '{
242+ action: "continue",
243+ messages: $messages,
244+ metadata: {hook_name: "validator", validated: true}
245+ }'
246+ ```
247+
248+ ### 5. Configuration Schema
249+
250+ ```yaml
251+ hooks:
252+ # Security settings for ALL hooks
253+ security:
254+ allowedPaths:
255+ - /usr/local/bin/mm-hooks
256+ - /home/user/.mm/hooks
257+ - /opt/mm/hooks
258+ allowedExtensions:
259+ - .sh
260+ - .py
261+ - .js
262+ - .lua
263+ - .rb
264+ - "" # Allow scripts without extension
265+ forbiddenExtensions:
266+ - .go # Prevent compiled Go hooks
267+ defaultTimeout: 5s
268+ maxOutputSize: 10240 # 10KB max per hook
269+
270+ # Hook definitions by type
271+ preGeneration:
272+ # Priority 10 = highest (executes first)
273+ - command: /path/to/security.py
274+ priority: 10
275+ mode: sync
276+ onError: fail
277+ timeout: 3s
278+
279+ # Priority 20 = second
280+ - command: /path/to/spellcheck.py
281+ priority: 20
282+ mode: sync
283+ onError: warn
284+ timeout: 2s
285+
286+ # Priority 30 = third
287+ - command: /path/to/context-injector.sh
288+ priority: 30
289+ mode: sync
290+ onError: warn
291+ timeout: 3s
292+
293+ # Priority 40 = fourth
294+ - command: /path/to/validator.py
295+ priority: 40
296+ mode: sync
297+ onError: fail
298+ timeout: 2s
299+
300+ # Priority 100 = last (async, non-blocking)
301+ - command: /path/to/logger.sh
302+ priority: 100
303+ mode: async
304+ onError: ignore
305+ timeout: 5s
306+ ```
307+
308+ **Priority System**: Lower number = higher priority = executes first
309+
310+ ### 6. Error Handling and Execution Modes
311+
312+ | Mode | Blocking | Error Behavior | Use Case |
313+ |------|----------|----------------|----------|
314+ | `sync` | Yes | According to `onError` (fail/warn/ignore) | Critical hooks |
315+ | `async` | No | Always log, never block | Non-critical hooks |
316+
317+ **onError values:**
318+ - `fail`: Stop execution, return error to user
319+ - `warn`: Log warning, continue execution
320+ - `ignore`: Silent continue
321+
322+ **Exit codes from hooks:**
323+ - `0`: Success
324+ - `1`: JSON parse error
325+ - `2`: Runtime error
326+ - `143`: Timeout (SIGTERM)
327+
328+ ### 7. Execution Flow
329+
330+ ```
331+ User Input: "delete all files"
332+ │
333+ ▼
334+ ┌─────────────────────────────────────────┐
335+ │ mini-me: Build JSON payload │
336+ │ {type: "preGeneration", current_text: ...}│
337+ └───────────────────────┬─────────────────┘
338+ │
339+ ▼
340+ ┌─────────────────────────────────────────┐
341+ │ mini-me: Execute hook #1 (security.py) │
342+ │ ├─ stdin: JSON payload │
343+ │ └─ stdout: JSON result │
344+ └───────────────────────┬─────────────────┘
345+ │
346+ ▼
347+ ┌─────────────────────────────────────────┐
348+ │ Hook 1 checks for patterns │
349+ │ if "rm -rf" found: │
350+ │ return {"action": "stop", ...} │
351+ │ else: │
352+ │ return {"action": "continue", ...} │
353+ └───────────────────────┬─────────────────┘
354+ │
355+ ┌───────────────┴───────────────┐
356+ ▼ ▼
357+ ┌─────────────────┐ ┌─────────────────┐
358+ │ Hook #2 │ │ STOP TURN │
359+ │ (spellcheck) │ │ Return response │
360+ └─────────────────┘ └─────────────────┘
361+ │
362+ ▼
363+ ┌─────────────────────────┐
364+ │ Hook #3 (context) │
365+ └───────────────────┬───────┘
366+ │
367+ ▼
368+ ┌───────────────────┐
369+ │ Hook #4 (validate)│
370+ └───────────────────┘
371+ │
372+ ▼
373+ ┌─────────────────────────────────────────┐
374+ │ engine.Generate() with final messages │
375+ │ (potentially modified by hooks) │
376+ └─────────────────────────────────────────┘
377+ ```
378+
379+ ### 8. Security Considerations
380+
381+ | Risk | Mitigation |
382+ |------|------------|
383+ | Malicious hook script | Allowlist of executable paths, allowed extensions |
384+ | Blocking hook (sync mode) | Configurable timeout (default: 5s) |
385+ | Resource exhaustion | Max output size limit (default: 10KB) |
386+ | Hook crashes | Process isolation, stderr captured separately |
387+ | Sensitive data in hooks | Hooks receive only configured payload fields |
388+
389+ ### 9. Implementation Sketch (Go)
390+
391+ ```go
392+ // internal/hooks/runner.go
393+
394+ type HookResult struct {
395+ Action string `json:"action"` // continue, stop, error
396+ Messages []*ai.Message `json:"messages,omitempty"`
397+ Response string `json:"response,omitempty"`
398+ Reason string `json:"reason,omitempty"`
399+ Error string `json:"error,omitempty"`
400+ ErrorType string `json:"error_type,omitempty"`
401+ Metadata map[string]interface{} `json:"metadata,omitempty"`
402+ }
403+
404+ type HookStopResult struct {
405+ Response string
406+ Reason string
407+ }
408+
409+ func (r *HookRunner) RunPreGenerationHooks(
410+ ctx context.Context,
411+ messages []*ai.Message,
412+ system string,
413+ tools []ai.ToolRef,
414+ sessionID string,
415+ workingDir string,
416+ ) ([]*ai.Message, *HookStopResult, error) {
417+
418+ payload := PreGenerationPayload{
419+ Hook: HookInfo{Type: "preGeneration", Timestamp: time.Now()},
420+ Context: HookContext{
421+ SessionID: sessionID,
422+ WorkingDir: workingDir,
423+ System: system,
424+ Tools: tools,
425+ },
426+ Payload: PreGenerationPayloadData{
427+ Messages: messages,
428+ CurrentMessage: messages[len(messages)-1],
429+ CurrentText: messages[len(messages)-1].Content[0].Text,
430+ },
431+ }
432+
433+ jsonInput, _ := json.Marshal(payload)
434+ hooks := r.getSortedHooks(HookTypePreGeneration)
435+ var finalMessages = messages
436+
437+ for _, hook := range hooks {
438+ result, err := r.runExternalHook(ctx, hook, jsonInput)
439+ if err != nil {
440+ if hook.Mode == "sync" && hook.OnError == "fail" {
441+ return nil, nil, err
442+ }
443+ log.Printf("[hooks] %s error: %v", hook.Command, err)
444+ continue
445+ }
446+
447+ switch result.Action {
448+ case "continue":
449+ finalMessages = result.Messages
450+ case "stop":
451+ return nil, &HookStopResult{
452+ Response: result.Response,
453+ Reason: result.Reason,
454+ }, nil
455+ case "error":
456+ if hook.Mode == "sync" && hook.OnError == "fail" {
457+ return nil, nil, fmt.Errorf("hook error: %s", result.Error)
458+ }
459+ }
460+ }
461+
462+ return finalMessages, nil, nil
463+ }
464+
465+ func (r *HookRunner) runExternalHook(
466+ ctx context.Context,
467+ hook HookConfig,
468+ input []byte,
469+ ) (HookResult, error) {
470+
471+ cmd := exec.CommandContext(ctx, hook.Command)
472+ cmd.Stdin = bytes.NewReader(input)
473+
474+ var stdoutBuf, stderrBuf bytes.Buffer
475+ cmd.Stdout = &stdoutBuf
476+ cmd.Stderr = &stderrBuf
477+
478+ err := cmd.Run()
479+
480+ if err != nil {
481+ return HookResult{}, fmt.Errorf("hook %s failed: %v (stderr: %s)",
482+ hook.Command, err, stderrBuf.String())
483+ }
484+
485+ var result HookResult
486+ if err := json.Unmarshal(stdoutBuf.Bytes(), &result); err != nil {
487+ return HookResult{}, fmt.Errorf("hook %s returned invalid JSON: %v",
488+ hook.Command, err)
489+ }
490+
491+ return result, nil
492+ }
493+ ```
494+
495+ ### 10. Summary: How User Requirements Are Met
496+
497+ | Requirement | Mechanism | Example |
498+ |------------|-----------|---------|
499+ | **Detect** patterns | Analyze `payload.current_text` in preGeneration hook | Block "rm -rf" |
500+ | **Modify/Retreat** message | Return modified `messages` array | Fix spelling, reformat |
501+ | **Add context** | Insert system messages into `messages` | Inject file contents |
502+ | **Stop turn** | Return `{action: "stop", response: "..."}` | Validation failed |
503+
504+ All capabilities work via **stdin/stdout JSON communication** with hooks in **any language**.
505+
506+ ### 11. Open Questions
507+
508+ 1. Should hooks have access to full conversation history (beyond current turn)?
509+ - Current: Only current turn messages
510+ - Option: Add `conversation_history` with configurable depth
511+
512+ 2. Should hooks be able to persist state between calls?
513+ - Current: Stateless
514+ - Option: Designated state directory for hooks
515+
516+ 3. Should hooks be able to execute tools?
517+ - Risk: Security
518+ - Suggestion: No, hooks should only observe/modify data
519+
520+ 4. Should there be a total timeout for all hooks of a type?
521+ - Current: Per-hook timeout
522+ - Option: Add `totalTimeout` for hook chains
523+
524+ 5. Should hooks be able to modify tool configurations?
525+ - Current: Tools are read-only
526+ - Suggestion: No, for security
13 tasks: []527 tasks: []
14 comments: []528 comments: []
modified .tickets/milestones.yaml +4 -1
@@ -7,5 +7,8 @@ milestones:
77 state: closed
88 dueOn: 2026-09-30
99 - title: v0.1.1
10- state: open
10+ state: closed
1111 dueOn: 2026-10-15
12+ - title: v0.1.2
13+ state: open
14+ dueOn: 2026-10-30
@@ -7,5 +7,8 @@ milestones:
7 state: closed7 state: closed
8 dueOn: 2026-09-308 dueOn: 2026-09-30
9 - title: v0.1.19 - title: v0.1.1
10- state: open10+ state: closed
11 dueOn: 2026-10-1511 dueOn: 2026-10-15
12+ - title: v0.1.2
13+ state: open
14+ dueOn: 2026-10-30