README.md

  1# Hooks Package
  2
  3A Git-like hooks system for Crush that allows users to intercept and modify behavior at key points in the application lifecycle.
  4
  5## Overview
  6
  7The hooks package provides a flexible, shell-based system for customizing Crush behavior through executable scripts. Hooks can:
  8
  9- Add context to LLM requests
 10- Control tool execution permissions
 11- Modify prompts and tool parameters
 12- Audit and log activity
 13- Execute cleanup on shutdown
 14
 15### Cross-Platform Support
 16
 17The hooks system works on **Windows**, **macOS**, and **Linux**:
 18
 19- **Hook Files**: All hooks must be `.sh` files (shell scripts)
 20- **Shell Execution**: Uses Crush's internal POSIX shell emulator ([mvdan.cc/sh](https://mvdan.cc/sh)) on all platforms
 21- **Hook Discovery**:
 22  - **Unix/macOS**: `.sh` files must have execute permission (`chmod +x hook.sh`)
 23  - **Windows**: `.sh` files are automatically recognized (no permission needed)
 24- **Path Separators**: Use forward slashes (`/`) in hook scripts for cross-platform compatibility
 25
 26**Example**:
 27```bash
 28# Works on Windows, macOS, and Linux
 29.crush/hooks/pre-tool-use/01-check.sh
 30```
 31
 32## Quick Start
 33
 34### Creating a Hook
 35
 361. Create an executable script in `.crush/hooks/{hook-type}/`:
 37
 38```bash
 39#!/bin/bash
 40# .crush/hooks/pre-tool-use/01-block-dangerous.sh
 41
 42if [ "$CRUSH_TOOL_NAME" = "bash" ]; then
 43  COMMAND=$(crush_get_tool_input command)
 44  if [[ "$COMMAND" =~ "rm -rf /" ]]; then
 45    crush_deny "Blocked dangerous command"
 46  fi
 47fi
 48```
 49
 502. Make it executable:
 51
 52```bash
 53chmod +x .crush/hooks/pre-tool-use/01-block-dangerous.sh
 54```
 55
 563. The hook will automatically execute when the event occurs.
 57
 58## Hook Types
 59
 60### 1. UserPromptSubmit
 61
 62**When**: After user submits prompt, before sending to LLM  
 63**Use cases**: Add context, modify prompts, validate input  
 64**Location**: `.crush/hooks/user-prompt-submit/`
 65
 66**Available data** (via stdin JSON):
 67- `prompt` - User's prompt text
 68- `attachments` - List of attached files
 69- `model` - Model name
 70- `provider` - Provider name (e.g., "anthropic", "openai")
 71- `is_first_message` - Boolean indicating if this is the first message in the conversation
 72
 73**Example**:
 74```bash
 75#!/bin/bash
 76# Add git context to every prompt, and README only for first message
 77
 78BRANCH=$(git branch --show-current 2>/dev/null)
 79if [ -n "$BRANCH" ]; then
 80  crush_add_context "Current branch: $BRANCH"
 81fi
 82
 83# Only add README context for the first message to avoid repetition
 84IS_FIRST=$(crush_get_input is_first_message)
 85if [ "$IS_FIRST" = "true" ] && [ -f "README.md" ]; then
 86  crush_add_context_file "README.md"
 87fi
 88```
 89
 90### 2. PreToolUse
 91
 92**When**: After LLM requests tool use, before permission check & execution  
 93**Use cases**: Auto-approve, deny dangerous commands, audit requests  
 94**Location**: `.crush/hooks/pre-tool-use/`
 95
 96**Available data** (via stdin JSON):
 97- `tool_input` - Tool parameters (object)
 98
 99**Environment variables**:
100- `$CRUSH_TOOL_NAME` - Name of the tool being called
101- `$CRUSH_TOOL_CALL_ID` - Unique ID for this tool call
102
103**Example**:
104```bash
105#!/bin/bash
106# Auto-approve read-only tools and modify parameters
107
108case "$CRUSH_TOOL_NAME" in
109  view|ls|grep|glob)
110    crush_approve "Auto-approved read-only tool"
111    ;;
112  bash)
113    COMMAND=$(crush_get_tool_input command)
114    if [[ "$COMMAND" =~ ^(ls|cat|grep) ]]; then
115      crush_approve "Auto-approved safe bash command"
116    fi
117    ;;
118  view)
119    # Limit file reads to 1000 lines max for performance
120    crush_modify_input "limit" "1000"
121    ;;
122esac
123```
124
125### 3. PostToolUse
126
127**When**: After tool executes, before result sent to LLM  
128**Use cases**: Filter output, redact secrets, log results  
129**Location**: `.crush/hooks/post-tool-use/`
130
131**Available data** (via stdin JSON):
132- `tool_input` - Tool parameters (object)
133- `tool_output` - Tool result (object with `success`, `content`)
134- `execution_time_ms` - How long the tool took
135
136**Environment variables**:
137- `$CRUSH_TOOL_NAME` - Name of the tool
138- `$CRUSH_TOOL_CALL_ID` - Unique ID for this tool call
139
140**Example**:
141```bash
142#!/bin/bash
143# Redact sensitive information from tool output
144
145# Get tool output using helper (stdin is automatically available)
146OUTPUT_CONTENT=$(crush_get_input tool_output | jq -r '.content // empty')
147
148# Check if output contains sensitive patterns
149if echo "$OUTPUT_CONTENT" | grep -qE '(password|api[_-]?key|secret|token)'; then
150  # Redact sensitive data
151  REDACTED=$(echo "$OUTPUT_CONTENT" | sed -E 's/(password|api[_-]?key|secret|token)[[:space:]]*[:=][[:space:]]*[^[:space:]]+/\1=\[REDACTED\]/gi')
152  crush_modify_output "content" "$REDACTED"
153  crush_log "Redacted sensitive information from $CRUSH_TOOL_NAME output"
154fi
155```
156
157### 4. Stop
158
159**When**: When agent conversation loop stops or is cancelled  
160**Use cases**: Save conversation state, cleanup session resources, archive logs  
161**Location**: `.crush/hooks/stop/`
162
163**Available data** (via stdin JSON):
164- `reason` - Why the loop stopped (e.g., "completed", "cancelled", "error")
165
166**Environment variables**:
167- `$CRUSH_SESSION_ID` - The session ID that stopped
168
169**Example**:
170```bash
171#!/bin/bash
172# Save conversation summary when agent loop stops
173
174REASON=$(crush_get_input reason)
175
176# Archive session logs
177if [ -f ".crush/session-$CRUSH_SESSION_ID.log" ]; then
178  ARCHIVE="logs/session-$CRUSH_SESSION_ID-$(date +%Y%m%d-%H%M%S).log"
179  mkdir -p logs
180  mv ".crush/session-$CRUSH_SESSION_ID.log" "$ARCHIVE"
181  gzip "$ARCHIVE"
182  crush_log "Archived session logs: $ARCHIVE.gz (reason: $REASON)"
183fi
184```
185
186## Catch-All Hooks
187
188Place hooks at the **root level** (`.crush/hooks/*.sh`) to run for **ALL hook types**:
189
190```bash
191#!/bin/bash
192# .crush/hooks/00-global-log.sh
193# This runs for every hook type
194
195echo "[$CRUSH_HOOK_TYPE] Session: $CRUSH_SESSION_ID" >> global.log
196```
197
198**Execution order**:
1991. Catch-all hooks (alphabetically sorted)
2002. Type-specific hooks (alphabetically sorted)
201
202Use `$CRUSH_HOOK_TYPE` to determine which event triggered the hook.
203
204## Helper Functions
205
206All hooks have access to these built-in functions (no sourcing required):
207
208### Permission Helpers
209
210#### `crush_approve [message]`
211Approve the current tool call (PreToolUse only).
212
213```bash
214crush_approve "Auto-approved read-only command"
215```
216
217#### `crush_deny [message]`
218Deny the current tool call and stop execution (PreToolUse only).
219
220```bash
221crush_deny "Blocked dangerous operation"
222# Script exits immediately with code 2
223```
224
225### Context Helpers
226
227#### `crush_add_context "content"`
228Add raw text content to LLM context.
229
230```bash
231crush_add_context "Project uses React 18 with TypeScript"
232```
233
234#### `crush_add_context_file "path"`
235Load a file and add its content to LLM context.
236
237```bash
238crush_add_context_file "docs/ARCHITECTURE.md"
239crush_add_context_file "package.json"
240```
241
242### Modification Helpers
243
244#### `crush_modify_prompt "new_prompt"`
245Replace the user's prompt (UserPromptSubmit only).
246
247```bash
248PROMPT=$(crush_get_prompt)
249MODIFIED="$PROMPT\n\nNote: Always use TypeScript."
250crush_modify_prompt "$MODIFIED"
251```
252
253#### `crush_modify_input "param_name" "value"`
254Modify tool input parameters (PreToolUse only).
255
256Values are parsed as JSON when valid, supporting all JSON types (strings, numbers, booleans, arrays, objects).
257
258```bash
259# Strings (no quotes needed for simple strings)
260crush_modify_input "command" "ls -la"
261crush_modify_input "working_dir" "/tmp"
262
263# Numbers (parsed as JSON)
264crush_modify_input "offset" "100"
265crush_modify_input "limit" "50"
266
267# Booleans (parsed as JSON)
268crush_modify_input "run_in_background" "true"
269crush_modify_input "replace_all" "false"
270
271# Arrays (JSON format)
272crush_modify_input "ignore" '["*.log","*.tmp"]'
273
274# Quoted strings (for strings with spaces or special chars)
275crush_modify_input "message" '"hello world"'
276```
277
278#### `crush_modify_output "field_name" "value"`
279Modify tool output before sending to LLM (PostToolUse only).
280
281```bash
282# Redact sensitive information from tool output content
283crush_modify_output "content" "[REDACTED - sensitive data removed]"
284
285# Can also modify other fields in the tool_output object
286crush_modify_output "success" "false"
287```
288
289#### `crush_stop [message]`
290Stop execution immediately.
291
292```bash
293if [ "$(date +%H)" -lt 9 ]; then
294  crush_stop "Crush is only available during business hours"
295fi
296```
297
298### Input Parsing Helpers
299
300Hooks receive JSON context via stdin, which is automatically saved and available to all helper functions. You can call multiple helpers without manually reading stdin first.
301
302#### `crush_get_input "field_name"`
303Get a top-level field from the hook context.
304
305```bash
306# Can call multiple times without saving stdin
307PROMPT=$(crush_get_input prompt)
308MODEL=$(crush_get_input model)
309```
310
311#### `crush_get_tool_input "parameter"`
312Get a tool parameter (PreToolUse/PostToolUse only).
313
314```bash
315# Can call multiple times without saving stdin
316COMMAND=$(crush_get_tool_input command)
317FILE_PATH=$(crush_get_tool_input file_path)
318```
319
320#### `crush_get_prompt`
321Get the user's prompt (UserPromptSubmit only).
322
323```bash
324PROMPT=$(crush_get_prompt)
325if [[ "$PROMPT" =~ "password" ]]; then
326  crush_stop "Never include passwords in prompts"
327fi
328```
329
330### Logging Helper
331
332#### `crush_log "message"`
333Write to Crush's log (stderr).
334
335```bash
336crush_log "Processing hook for tool: $CRUSH_TOOL_NAME"
337```
338
339## Environment Variables
340
341All hooks have access to these environment variables:
342
343### Always Available
344- `$CRUSH_HOOK_TYPE` - Type of hook: `user-prompt-submit`, `pre-tool-use`, `post-tool-use`, `stop`
345- `$CRUSH_SESSION_ID` - Current session ID
346- `$CRUSH_WORKING_DIR` - Working directory
347
348### Tool Hooks (PreToolUse, PostToolUse)
349- `$CRUSH_TOOL_NAME` - Name of the tool being called
350- `$CRUSH_TOOL_CALL_ID` - Unique ID for this tool call
351
352## Result Communication
353
354Hooks communicate results back to Crush in two ways:
355
356### 1. Environment Variables (Simple)
357
358Export variables to set hook results:
359
360```bash
361export CRUSH_PERMISSION=approve
362export CRUSH_MESSAGE="Auto-approved"
363export CRUSH_CONTINUE=false
364export CRUSH_CONTEXT_CONTENT="Additional context"
365export CRUSH_CONTEXT_FILES="/path/to/file1.md:/path/to/file2.md"
366```
367
368**Available variables**:
369- `CRUSH_PERMISSION` - `approve` or `deny`
370- `CRUSH_MESSAGE` - User-facing message
371- `CRUSH_CONTINUE` - `true` or `false` (stop execution)
372- `CRUSH_MODIFIED_PROMPT` - New prompt text
373- `CRUSH_MODIFIED_INPUT` - Modified tool input (format: `key=value:key2=value2`, values parsed as JSON)
374- `CRUSH_MODIFIED_OUTPUT` - Modified tool output (format: `key=value:key2=value2`, values parsed as JSON)
375- `CRUSH_CONTEXT_CONTENT` - Text to add to LLM context
376- `CRUSH_CONTEXT_FILES` - Colon-separated file paths
377
378**Note**: `CRUSH_MODIFIED_INPUT` and `CRUSH_MODIFIED_OUTPUT` use `:` as delimiter between pairs. For complex values with multiple fields or nested structures, use JSON output instead (see below).
379
380### 2. JSON Output (Complex)
381
382Echo JSON to stdout for complex modifications:
383
384```bash
385echo '{
386  "permission": "approve",
387  "message": "Modified command",
388  "modified_input": {
389    "command": "ls -la --color=auto"
390  },
391  "context_content": "Added context"
392}'
393```
394
395**JSON fields**:
396- `continue` (bool) - Continue execution
397- `permission` (string) - `approve` or `deny`
398- `message` (string) - User-facing message
399- `modified_prompt` (string) - New prompt
400- `modified_input` (object) - Modified tool parameters
401- `modified_output` (object) - Modified tool results
402- `context_content` (string) - Context to add
403- `context_files` (array) - File paths to load
404
405**Note**: Environment variables and JSON output are merged automatically.
406
407## Exit Codes
408
409- **0** - Success, continue execution
410- **1** - Error (PreToolUse: denies permission, others: logs and continues)
411- **2** - Deny/stop execution (sets `Continue=false`)
412
413```bash
414# Example: Check rate limit
415COUNT=$(grep -c "$(date +%Y-%m-%d)" usage.log)
416if [ "$COUNT" -gt 100 ]; then
417  echo "Rate limit exceeded" >&2
418  exit 2  # Stops execution
419fi
420```
421
422## Hook Ordering
423
424Hooks execute **sequentially** in alphabetical order. Use numeric prefixes to control order:
425
426```
427.crush/hooks/
428  00-global-log.sh          # Catch-all: runs first for all types
429  pre-tool-use/
430    01-rate-limit.sh        # Runs first
431    02-auto-approve.sh      # Runs second
432    99-audit.sh             # Runs last
433```
434
435## Result Merging
436
437When multiple hooks execute, their results are merged:
438
439### Permission (Most Restrictive Wins)
440- `deny` > `approve`
441- If any hook denies, the final result is deny
442- If any hook approves and no denials, the result is approve
443- If no hooks set permission, normal permission flow applies
444
445### Continue (AND Logic)
446- All hooks must set `Continue=true` (or not set it)
447- If any hook sets `Continue=false`, execution stops
448
449### Context (Append)
450- Context content from all hooks is concatenated
451- Context files from all hooks are combined
452
453### Messages (Append)
454- Messages are joined with `; ` separator
455
456### Modified Fields (Last Wins)
457- Modified prompt: last hook's value wins
458- Modified input/output: maps are merged, last value wins for conflicts
459
460## Configuration
461
462Configure hooks in `crush.json`:
463
464```json
465{
466  "hooks": {
467    "disabled": false,
468    "timeout_seconds": 30,
469    "directories": [
470      "/path/to/custom/hooks",
471      ".crush/hooks"
472    ],
473    "disable_hooks": [
474      "pre-tool-use/slow-check.sh",
475      "user-prompt-submit/verbose.sh"
476    ],
477    "environment": {
478      "CUSTOM_VAR": "value"
479    },
480    "inline": {
481      "pre-tool-use": [{
482        "name": "rate-limit",
483        "script": "#!/bin/bash\n# Inline hook script here..."
484      }]
485    }
486  }
487}
488```
489
490### Configuration Options
491
492- **disabled** (bool) - Disable hook execution entirely (default: `false`)
493- **timeout_seconds** (int) - Maximum execution time per hook (default: `30`)
494- **directories** ([]string) - Directories to search for hook scripts (defaults to `[".crush/hooks"]`)
495- **disable_hooks** ([]string) - List of hook paths to skip (relative to hooks directory)
496- **environment** (map) - Environment variables to pass to all hooks
497- **inline** (map) - Hooks defined directly in config (by hook type)
498
499## Best Practices
500
501### 1. Keep Hooks Fast
502Hooks run synchronously. Keep them under 1 second to avoid slowing down the UI.
503
504```bash
505# Bad: Slow network call
506curl -X POST https://api.example.com/log
507
508# Good: Log locally, sync in background
509echo "$LOG_ENTRY" >> audit.log
510```
511
512### 2. Handle Errors Gracefully
513Don't let hooks crash. Use error handling:
514
515```bash
516BRANCH=$(git branch --show-current 2>/dev/null)
517if [ -n "$BRANCH" ]; then
518  crush_add_context "Branch: $BRANCH"
519fi
520```
521
522### 3. Use Descriptive Names
523Use numeric prefixes and descriptive names:
524
525```bash
52601-security-check.sh      # Good
52799-audit-log.sh           # Good
528hook.sh                   # Bad
529```
530
531### 4. Test Hooks Independently
532Run hooks manually to test:
533
534```bash
535export CRUSH_HOOK_TYPE=pre-tool-use
536export CRUSH_TOOL_NAME=bash
537echo '{"tool_input":{"command":"rm -rf /"}}' | .crush/hooks/pre-tool-use/01-block-dangerous.sh
538echo "Exit code: $?"
539```
540
541### 5. Log for Debugging
542Use `crush_log` to debug hook execution:
543
544```bash
545crush_log "Checking command: $COMMAND"
546if [[ "$COMMAND" =~ "dangerous" ]]; then
547  crush_log "Blocking dangerous command"
548  crush_deny "Command blocked"
549fi
550```
551
552### 6. Don't Block on I/O
553Avoid blocking operations:
554
555```bash
556# Bad: Waits for user input
557read -p "Continue? " answer
558
559# Bad: Long-running process
560./expensive-analysis.sh
561
562# Good: Quick checks
563[ -f ".allowed" ] && crush_approve
564```