SKILL.md

  1---
  2name: crush-hooks
  3description: Create, debug, and configure Crush hooks (user-defined shell commands that fire before tool execution). Use when the user wants to add a hook, write a hook script, troubleshoot hook behavior, or configure hooks in crush.json.
  4---
  5
  6# Crush Hooks
  7
  8Hooks are user-defined shell commands in `crush.json` that fire at specific
  9points during execution, giving deterministic control over tool behavior. Hooks
 10run before permission checks.
 11
 12## Supported Events
 13
 14Only `PreToolUse` is currently supported. It fires before every tool call.
 15
 16Event names are case-insensitive and accept snake_case:
 17`PreToolUse`, `pretooluse`, `pre_tool_use`, `PRE_TOOL_USE` all work.
 18
 19## Configuration
 20
 21Add hooks to `crush.json` (project-level or global). Project-level hooks take
 22precedence.
 23
 24```jsonc
 25{
 26  "hooks": {
 27    "PreToolUse": [
 28      {
 29        "matcher": "^bash$",                // regex against tool name (optional; omit to match all)
 30        "command": "./hooks/my-hook.sh",     // required: shell command to run
 31        "timeout": 10                        // optional: seconds, default 30
 32      }
 33    ]
 34  }
 35}
 36```
 37
 38Only `command` is required. Omit `matcher` to match all tools.
 39
 40## Writing Hook Scripts
 41
 42### Input
 43
 44Hooks receive data two ways:
 45
 46**Environment variables:**
 47
 48| Variable                     | Description                              |
 49| ---------------------------- | ---------------------------------------- |
 50| `CRUSH_EVENT`                | Event name (e.g. `PreToolUse`)           |
 51| `CRUSH_TOOL_NAME`            | Tool being called (e.g. `bash`)          |
 52| `CRUSH_SESSION_ID`           | Current session ID                       |
 53| `CRUSH_CWD`                  | Working directory                        |
 54| `CRUSH_PROJECT_DIR`          | Project root directory                   |
 55| `CRUSH_TOOL_INPUT_COMMAND`   | For `bash` calls: the shell command      |
 56| `CRUSH_TOOL_INPUT_FILE_PATH` | For file tools: the target file path     |
 57
 58**JSON on stdin:**
 59
 60```json
 61{
 62  "event": "PreToolUse",
 63  "session_id": "313909e",
 64  "cwd": "/home/user/project",
 65  "tool_name": "bash",
 66  "tool_input": {"command": "rm -rf /"}
 67}
 68```
 69
 70Parse with `jq`:
 71
 72```bash
 73read -r input
 74tool_name=$(echo "$input" | jq -r '.tool_name')
 75command=$(echo "$input" | jq -r '.tool_input.command // empty')
 76```
 77
 78### Output
 79
 80**Exit codes:**
 81
 82| Exit Code | Meaning                                                    |
 83| --------- | ---------------------------------------------------------- |
 84| 0         | Success. Stdout is parsed as JSON (see below).             |
 85| 2         | Block the tool. Stderr is used as the deny reason.         |
 86| Other     | Non-blocking error. Logged and ignored; tool call proceeds. |
 87
 88**Simplest form** — block with exit code 2 and stderr:
 89
 90```bash
 91if some_bad_condition; then
 92  echo "Blocked: reason here" >&2
 93  exit 2
 94fi
 95```
 96
 97**JSON form** — exit 0 with a JSON object on stdout for more control:
 98
 99```json
100{
101  "decision": "allow",
102  "reason": "not allowed",
103  "context": "Extra info appended to tool result",
104  "updated_input": {"command": "rewritten command"}
105}
106```
107
108- `decision`: `"allow"`, `"deny"`, or omit for no opinion.
109- `reason`: Shown to the model when denying.
110- `context`: Appended to the tool response the model sees.
111- `updated_input`: Replaces tool input before execution.
112
113### Multiple Hooks
114
115When multiple hooks match the same tool call:
116
117- **Deny wins** over allow; allow wins over no opinion.
118- All deny reasons are concatenated (newline-separated).
119- All context strings are concatenated.
120- Last non-empty `updated_input` wins. Ignored if denied.
121
122### Timeouts
123
124Default: 30 seconds. If exceeded, the hook is killed and treated as
125a non-blocking error (tool call proceeds).
126
127## Hook Templates
128
129When creating hooks, always include `#!/usr/bin/env bash`, use `set -euo pipefail`,
130and make the file executable (`chmod +x`).
131
132### Block a dangerous command
133
134```bash
135#!/usr/bin/env bash
136set -euo pipefail
137
138# Block rm -rf against root.
139if echo "$CRUSH_TOOL_INPUT_COMMAND" | grep -qE 'rm\s+-(rf|fr)\s+/'; then
140  echo "Refusing to run rm -rf against root" >&2
141  exit 2
142fi
143
144echo '{"decision": "allow"}'
145```
146
147Config: `{"matcher": "^bash$", "command": "./hooks/no-rm-rf.sh"}`
148
149### Inject context into file writes
150
151```bash
152#!/usr/bin/env bash
153set -euo pipefail
154
155# Remind about formatting when editing Go files.
156if [[ "$CRUSH_TOOL_INPUT_FILE_PATH" == *.go ]]; then
157  echo '{"decision": "allow", "context": "Remember: run gofumpt after editing Go files."}'
158else
159  echo '{}'
160fi
161```
162
163Config: `{"matcher": "^(edit|write|multiedit)$", "command": "./hooks/go-context.sh"}`
164
165### Block all MCP tools (inline)
166
167```jsonc
168{"matcher": "^mcp_", "command": "echo 'MCP tools are disabled' >&2; exit 2"}
169```
170
171### Log every tool call (inline)
172
173```jsonc
174{"command": "echo \"$(date -Iseconds) $CRUSH_TOOL_NAME\" >> ./tools.log"}
175```
176
177### Rewrite tool input
178
179```bash
180#!/usr/bin/env bash
181set -euo pipefail
182
183read -r input
184original_cmd=$(echo "$input" | jq -r '.tool_input.command')
185rewritten=$(some-rewriter "$original_cmd")
186
187cat <<EOF
188{
189  "decision": "allow",
190  "context": "Rewrote command for efficiency",
191  "updated_input": {"command": "$rewritten"}
192}
193EOF
194```
195
196### Restrict file writes to a directory
197
198```bash
199#!/usr/bin/env bash
200set -euo pipefail
201
202FILE_PATH="${CRUSH_TOOL_INPUT_FILE_PATH:-}"
203
204if [ -z "$FILE_PATH" ]; then
205  exit 0
206fi
207
208ALLOWED_DIR="./src"
209
210case "$FILE_PATH" in
211  "$ALLOWED_DIR"/*)
212    echo '{"decision": "allow"}'
213    ;;
214  *)
215    echo "Writes outside $ALLOWED_DIR are not allowed" >&2
216    exit 2
217    ;;
218esac
219```
220
221Config: `{"matcher": "^(edit|write|multiedit)$", "command": "./hooks/restrict-writes.sh"}`
222
223## Checklist for Creating Hooks
224
2251. Create the hook script (or use an inline command).
2262. Add `#!/usr/bin/env bash` and `set -euo pipefail` for shell scripts.
2273. Make the script executable: `chmod +x ./hooks/my-hook.sh`.
2284. Add the hook entry to `crush.json` under `hooks.PreToolUse`.
2295. Set `matcher` to a regex matching the target tool names, or omit for all tools.
2306. Test the hook by triggering the relevant tool call.
231
232## Debugging Hooks
233
234- Hooks that exceed their timeout are killed silently; increase `timeout` if needed.
235- Non-zero exit codes other than 2 are logged but don't block — check Crush logs.
236- Use `echo "debug info" >&2` for stderr logging that won't interfere with JSON output.
237- Verify `matcher` regex matches the intended tool name (e.g. `^bash$` not `bash`
238  if you only want the bash tool, not `mcp_something_bash`).
239
240## Claude Code Compatibility
241
242Crush also accepts the Claude Code hook output format:
243
244```json
245{
246  "hookSpecificOutput": {
247    "permissionDecision": "allow",
248    "permissionDecisionReason": "Auto-approved",
249    "updatedInput": {"command": "echo rewritten"}
250  }
251}
252```
253
254Existing Claude Code hooks work without modification.