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.