|
|
4 ヶ月 前 | |
|---|---|---|
| .. | ||
| README.md | 4 ヶ月 前 | |
| builtins.go | 4 ヶ月 前 | |
| builtins_test.go | 4 ヶ月 前 | |
| config.go | 4 ヶ月 前 | |
| examples_test.go | 4 ヶ月 前 | |
| executor.go | 4 ヶ月 前 | |
| executor_test.go | 4 ヶ月 前 | |
| helpers.sh | 4 ヶ月 前 | |
| manager.go | 4 ヶ月 前 | |
| manager_test.go | 4 ヶ月 前 | |
| parser.go | 4 ヶ月 前 | |
| parser_test.go | 4 ヶ月 前 | |
| types.go | 4 ヶ月 前 | |
A Git-like hooks system for Crush that allows users to intercept and modify behavior at key points in the application lifecycle.
The hooks package provides a flexible, shell-based system for customizing Crush behavior through executable scripts. Hooks can:
The hooks system works on Windows, macOS, and Linux:
.sh files (shell scripts).sh files must have execute permission (chmod +x hook.sh).sh files are automatically recognized (no permission needed)/) in hook scripts for cross-platform compatibilityExample:
# Works on Windows, macOS, and Linux
.crush/hooks/pre-tool-use/01-check.sh
Create an executable script in .crush/hooks/{hook-type}/:
#!/bin/bash
# .crush/hooks/pre-tool-use/01-block-dangerous.sh
if [ "$CRUSH_TOOL_NAME" = "bash" ]; then
COMMAND=$(crush_get_tool_input command)
if [[ "$COMMAND" =~ "rm -rf /" ]]; then
crush_deny "Blocked dangerous command"
fi
fi
Make it executable:
chmod +x .crush/hooks/pre-tool-use/01-block-dangerous.sh
The hook will automatically execute when the event occurs.
When: After user submits prompt, before sending to LLM
Use cases: Add context, modify prompts, validate input
Location: .crush/hooks/user-prompt-submit/
Available data (via stdin JSON):
prompt - User's prompt textattachments - List of attached filesmodel - Model nameprovider - Provider name (e.g., "anthropic", "openai")is_first_message - Boolean indicating if this is the first message in the conversationExample:
#!/bin/bash
# Add git context to every prompt, and README only for first message
BRANCH=$(git branch --show-current 2>/dev/null)
if [ -n "$BRANCH" ]; then
crush_add_context "Current branch: $BRANCH"
fi
# Only add README context for the first message to avoid repetition
IS_FIRST=$(crush_get_input is_first_message)
if [ "$IS_FIRST" = "true" ] && [ -f "README.md" ]; then
crush_add_context_file "README.md"
fi
When: After LLM requests tool use, before permission check & execution
Use cases: Auto-approve, deny dangerous commands, audit requests
Location: .crush/hooks/pre-tool-use/
Available data (via stdin JSON):
tool_input - Tool parameters (object)Environment variables:
$CRUSH_TOOL_NAME - Name of the tool being called$CRUSH_TOOL_CALL_ID - Unique ID for this tool callExample:
#!/bin/bash
# Auto-approve read-only tools and modify parameters
case "$CRUSH_TOOL_NAME" in
view|ls|grep|glob)
crush_approve "Auto-approved read-only tool"
;;
bash)
COMMAND=$(crush_get_tool_input command)
if [[ "$COMMAND" =~ ^(ls|cat|grep) ]]; then
crush_approve "Auto-approved safe bash command"
fi
;;
view)
# Limit file reads to 1000 lines max for performance
crush_modify_input "limit" "1000"
;;
esac
When: After tool executes, before result sent to LLM
Use cases: Filter output, redact secrets, log results
Location: .crush/hooks/post-tool-use/
Available data (via stdin JSON):
tool_input - Tool parameters (object)tool_output - Tool result (object with success, content)execution_time_ms - How long the tool tookEnvironment variables:
$CRUSH_TOOL_NAME - Name of the tool$CRUSH_TOOL_CALL_ID - Unique ID for this tool callExample:
#!/bin/bash
# Redact sensitive information from tool output
# Get tool output using helper (stdin is automatically available)
OUTPUT_CONTENT=$(crush_get_input tool_output | jq -r '.content // empty')
# Check if output contains sensitive patterns
if echo "$OUTPUT_CONTENT" | grep -qE '(password|api[_-]?key|secret|token)'; then
# Redact sensitive data
REDACTED=$(echo "$OUTPUT_CONTENT" | sed -E 's/(password|api[_-]?key|secret|token)[[:space:]]*[:=][[:space:]]*[^[:space:]]+/\1=\[REDACTED\]/gi')
crush_modify_output "content" "$REDACTED"
crush_log "Redacted sensitive information from $CRUSH_TOOL_NAME output"
fi
When: When agent conversation loop stops or is cancelled
Use cases: Save conversation state, cleanup session resources, archive logs
Location: .crush/hooks/stop/
Available data (via stdin JSON):
reason - Why the loop stopped (e.g., "completed", "cancelled", "error")Environment variables:
$CRUSH_SESSION_ID - The session ID that stoppedExample:
#!/bin/bash
# Save conversation summary when agent loop stops
REASON=$(crush_get_input reason)
# Archive session logs
if [ -f ".crush/session-$CRUSH_SESSION_ID.log" ]; then
ARCHIVE="logs/session-$CRUSH_SESSION_ID-$(date +%Y%m%d-%H%M%S).log"
mkdir -p logs
mv ".crush/session-$CRUSH_SESSION_ID.log" "$ARCHIVE"
gzip "$ARCHIVE"
crush_log "Archived session logs: $ARCHIVE.gz (reason: $REASON)"
fi
Place hooks at the root level (.crush/hooks/*.sh) to run for ALL hook types:
#!/bin/bash
# .crush/hooks/00-global-log.sh
# This runs for every hook type
echo "[$CRUSH_HOOK_TYPE] Session: $CRUSH_SESSION_ID" >> global.log
Execution order:
Use $CRUSH_HOOK_TYPE to determine which event triggered the hook.
All hooks have access to these built-in functions (no sourcing required):
crush_approve [message]Approve the current tool call (PreToolUse only).
crush_approve "Auto-approved read-only command"
crush_deny [message]Deny the current tool call and stop execution (PreToolUse only).
crush_deny "Blocked dangerous operation"
# Script exits immediately with code 2
crush_add_context "content"Add raw text content to LLM context.
crush_add_context "Project uses React 18 with TypeScript"
crush_add_context_file "path"Load a file and add its content to LLM context.
crush_add_context_file "docs/ARCHITECTURE.md"
crush_add_context_file "package.json"
crush_modify_prompt "new_prompt"Replace the user's prompt (UserPromptSubmit only).
PROMPT=$(crush_get_prompt)
MODIFIED="$PROMPT\n\nNote: Always use TypeScript."
crush_modify_prompt "$MODIFIED"
crush_modify_input "param_name" "value"Modify tool input parameters (PreToolUse only).
Values are parsed as JSON when valid, supporting all JSON types (strings, numbers, booleans, arrays, objects).
# Strings (no quotes needed for simple strings)
crush_modify_input "command" "ls -la"
crush_modify_input "working_dir" "/tmp"
# Numbers (parsed as JSON)
crush_modify_input "offset" "100"
crush_modify_input "limit" "50"
# Booleans (parsed as JSON)
crush_modify_input "run_in_background" "true"
crush_modify_input "replace_all" "false"
# Arrays (JSON format)
crush_modify_input "ignore" '["*.log","*.tmp"]'
# Quoted strings (for strings with spaces or special chars)
crush_modify_input "message" '"hello world"'
crush_modify_output "field_name" "value"Modify tool output before sending to LLM (PostToolUse only).
# Redact sensitive information from tool output content
crush_modify_output "content" "[REDACTED - sensitive data removed]"
# Can also modify other fields in the tool_output object
crush_modify_output "success" "false"
crush_stop [message]Stop execution immediately.
if [ "$(date +%H)" -lt 9 ]; then
crush_stop "Crush is only available during business hours"
fi
Hooks 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.
crush_get_input "field_name"Get a top-level field from the hook context.
# Can call multiple times without saving stdin
PROMPT=$(crush_get_input prompt)
MODEL=$(crush_get_input model)
crush_get_tool_input "parameter"Get a tool parameter (PreToolUse/PostToolUse only).
# Can call multiple times without saving stdin
COMMAND=$(crush_get_tool_input command)
FILE_PATH=$(crush_get_tool_input file_path)
crush_get_promptGet the user's prompt (UserPromptSubmit only).
PROMPT=$(crush_get_prompt)
if [[ "$PROMPT" =~ "password" ]]; then
crush_stop "Never include passwords in prompts"
fi
crush_log "message"Write to Crush's log (stderr).
crush_log "Processing hook for tool: $CRUSH_TOOL_NAME"
All hooks have access to these environment variables:
$CRUSH_HOOK_TYPE - Type of hook: user-prompt-submit, pre-tool-use, post-tool-use, stop$CRUSH_SESSION_ID - Current session ID$CRUSH_WORKING_DIR - Working directory$CRUSH_TOOL_NAME - Name of the tool being called$CRUSH_TOOL_CALL_ID - Unique ID for this tool callHooks communicate results back to Crush in two ways:
Export variables to set hook results:
export CRUSH_PERMISSION=approve
export CRUSH_MESSAGE="Auto-approved"
export CRUSH_CONTINUE=false
export CRUSH_CONTEXT_CONTENT="Additional context"
export CRUSH_CONTEXT_FILES="/path/to/file1.md:/path/to/file2.md"
Available variables:
CRUSH_PERMISSION - approve or denyCRUSH_MESSAGE - User-facing messageCRUSH_CONTINUE - true or false (stop execution)CRUSH_MODIFIED_PROMPT - New prompt textCRUSH_MODIFIED_INPUT - Modified tool input (format: key=value:key2=value2, values parsed as JSON)CRUSH_MODIFIED_OUTPUT - Modified tool output (format: key=value:key2=value2, values parsed as JSON)CRUSH_CONTEXT_CONTENT - Text to add to LLM contextCRUSH_CONTEXT_FILES - Colon-separated file pathsNote: 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).
Echo JSON to stdout for complex modifications:
echo '{
"permission": "approve",
"message": "Modified command",
"modified_input": {
"command": "ls -la --color=auto"
},
"context_content": "Added context"
}'
JSON fields:
continue (bool) - Continue executionpermission (string) - approve or denymessage (string) - User-facing messagemodified_prompt (string) - New promptmodified_input (object) - Modified tool parametersmodified_output (object) - Modified tool resultscontext_content (string) - Context to addcontext_files (array) - File paths to loadNote: Environment variables and JSON output are merged automatically.
2 - Deny/stop execution (sets Continue=false)
# Example: Check rate limit
COUNT=$(grep -c "$(date +%Y-%m-%d)" usage.log)
if [ "$COUNT" -gt 100 ]; then
echo "Rate limit exceeded" >&2
exit 2 # Stops execution
fi
Hooks execute sequentially in alphabetical order. Use numeric prefixes to control order:
.crush/hooks/
00-global-log.sh # Catch-all: runs first for all types
pre-tool-use/
01-rate-limit.sh # Runs first
02-auto-approve.sh # Runs second
99-audit.sh # Runs last
When multiple hooks execute, their results are merged:
deny > approveContinue=true (or not set it)Continue=false, execution stops; separatorConfigure hooks in crush.json:
{
"hooks": {
"disabled": false,
"timeout_seconds": 30,
"directories": [
"/path/to/custom/hooks",
".crush/hooks"
],
"disable_hooks": [
"pre-tool-use/slow-check.sh",
"user-prompt-submit/verbose.sh"
],
"environment": {
"CUSTOM_VAR": "value"
},
"inline": {
"pre-tool-use": [{
"name": "rate-limit",
"script": "#!/bin/bash\n# Inline hook script here..."
}]
}
}
}
false)30)[".crush/hooks"])Hooks run synchronously. Keep them under 1 second to avoid slowing down the UI.
# Bad: Slow network call
curl -X POST https://api.example.com/log
# Good: Log locally, sync in background
echo "$LOG_ENTRY" >> audit.log
Don't let hooks crash. Use error handling:
BRANCH=$(git branch --show-current 2>/dev/null)
if [ -n "$BRANCH" ]; then
crush_add_context "Branch: $BRANCH"
fi
Use numeric prefixes and descriptive names:
01-security-check.sh # Good
99-audit-log.sh # Good
hook.sh # Bad
Run hooks manually to test:
export CRUSH_HOOK_TYPE=pre-tool-use
export CRUSH_TOOL_NAME=bash
echo '{"tool_input":{"command":"rm -rf /"}}' | .crush/hooks/pre-tool-use/01-block-dangerous.sh
echo "Exit code: $?"
Use crush_log to debug hook execution:
crush_log "Checking command: $COMMAND"
if [[ "$COMMAND" =~ "dangerous" ]]; then
crush_log "Blocking dangerous command"
crush_deny "Command blocked"
fi
Avoid blocking operations:
# Bad: Waits for user input
read -p "Continue? " answer
# Bad: Long-running process
./expensive-analysis.sh
# Good: Quick checks
[ -f ".allowed" ] && crush_approve