@@ -1,38 +1,97 @@
11# Hooks
22
3−> [!NOTE] This document was designed for both humans and agents.
3+> [!NOTE]
4+> This document was designed for both humans and agents.
45
5−Hooks are user-defined shell commands that fire at specific points during
6−Crush's execution, giving you deterministic control over an agent's wily
7−behavior.
6+Hooks are user-defined shell scripts that run when various events happen during
7+the agent lifecycle, allowing you to both build on top of Crush, customize
8+its behavior, and exert deterministic control over an agent's wily behavior.
89
9−Hooks in Crush are shell-based, with a focus on simplicity. This allows hooks to
10−effectively be written in any language. In this document we'll primarily focus
11−on Bash for simplicity's sake, though we'll include some examples in other
12−languages at the end, too.
10+Hooks are just shell commands, and were designed to be both simple and future
11+forward.
1312
1413### Hot Hook Facts
1514
16−- Hooks run before permission checks. If a hook denies a tool call, you'll
17− never see a permission prompt for it. If a hook explicitly allows a tool
18− call, you'll _also_ never see a prompt — Crush treats `"decision": "allow"`
19− as affirmative pre-approval.
20−- Hooks only fire on the **top-level agent's** tool calls. Sub-agents (the
21− `agent` task tool, `agentic_fetch`, etc.) run without hook interception so
22− a single delegated turn doesn't trigger your hook N times. The outer
23− sub-agent tool call itself _is_ hooked, so policy like "never let the
24− agent spawn sub-agents" still works.
25−- Hooks are also compatible with hooks in Claude Code, however this document
26− covers the Crush-specific API only. One intentional divergence: Crush treats
27− `updated_input` as a shallow-merge patch rather than a full replacement — see
28− [Output](#output) below.
15+- Hooks just shell commands
16+- Hooks can be written in any language because they’re just executables: Bash, Python, Node, Rust, Haskell, whatever
17+- Hooks are Claude Code-compatible
18+- Crush ships with a builtin `crush-hook` skill write, edit, and configure
19+ hooks; just tell Crush how to configure Crush
2920- Crush currently supports just one hook, `PreToolUse`, with plans to support
30− the full gamut. If there's a hook you'd like to see, let us know.
21+ the full gamut; please let us know which hooks you'd like to see next
22+- Hooks run in parallel for speed, but their results compose in config order
23+ for determinism
24+
25+### Some things you can do with hooks:
26+
27+- Block "dangerous" commands: no more `git push -f` or `cabal init`
28+- Rewrite tool input: turn `node` calls info `deno`, scrub secrets from
29+ commands, rewrite all mentions of "Haskell" into "Haskell, The Best
30+ Language", and so on
31+- Inject context: add notes to the model's context whenever certain tools are
32+ called. For example: "remember to run gofumpt after editing Go files"
33+- Auto-approve tools: skip the permission prompt for bash commands that
34+ you know are safe
35+- Log certain tool calls
36+
37+…And lots more. Show us what you're building!
38+
39+## Baby's First Hook
40+
41+Let's just dive into it and make a simple hook. This particular hook will
42+disallow the use of Haskell (but we love you, Simon Peyton Jones).
43+
44+### Config
45+
46+The first thing we need to do is hook up our hook. Let's add the following to
47+our local `crush.json`. You can, of course, do this globally, too.
48+
49+```jsonc
50+{
51+ // As expected, hooks go in a "hooks" object.
52+ "hooks": {
53+ // PreToolUse is an event that fires before a tool is used.
54+ "PreToolUse": [
55+ {
56+ // What tool do we want to hook into? In this case, Bash, because it
57+ // runs the stuff we wanna block.
58+ "matcher": "^bash$",
59+
60+ // The path to our actual hook script.
61+ "command": "./no-haskell.sh",
62+ },
63+ ],
64+ },
65+}
66+```
67+
68+Now, let's make our `no-haskell.sh` hook script.
69+
70+```bash
71+#!/usr/bin/env bash
72+
73+# Disallow ghc, cabal, and stack. Pipe the bash command output
74+# ($CRUSH_TOOL_INPUT_COMMAND) to grep and match on a regexp.
75+if echo "$CRUSH_TOOL_INPUT_COMMAND" | grep -qE '(^| )((ghc|cabal|stack)(\.exe)?)( |$)'; then
76+
77+ # Someone is trying to use Haskell. Let's send a message back to the model
78+ # and user explaining why we're blocking this. Note that we send all feedback
79+ # like this to stderr.
80+ echo "No Haskell allowed, kiddo." >&2
81+
82+ # Now, block the tool call by exiting with code 2.
83+ exit 2
84+fi
85+```
86+
87+That's basically it. For the full guide on how hooks work, however, read on.
88+
89+---
3190
3291## Configuration
3392
34−Hooks can be added to your `crush.json` at both the global and project-level,
35−with project level hooks taking precedence.
93+Hooks can be added to your `crush.json` (or `.crush.json`) at both the global
94+and project-level, with project level hooks taking precedence.
3695
3796```jsonc
3897{
@@ -48,8 +107,8 @@ with project level hooks taking precedence.
48107}
49108```
50109
51−Hooks are keyed by event name. Only `command` is required; omit `matcher` to
52−match all tools.
110+Remember, hooks will run in parallel but resolve in config order. Last hook
111+wins when rewriting input, but first deny wins when blocking.
53112
54113## Events
55114
@@ -58,29 +117,24 @@ Here are the events you can hook into (spoiler: there's currently just one):
58117### PreToolUse
59118
60119This hook fires before every tool call. Use it to block dangerous commands,
61−enforce policies, rewrite tool input, or inject context the model should see.
120+enforce policies, rewrite tool input, inject context the model should see, log
121+stuff, and so on.
62122
63123**Matched against**: the tool name (e.g. `bash`, `edit`, `write`,
64124`mcp_github_create_pull_request`).
65125
66−> [!NOTE] Event names are case insensitive and snake-caseable, so `PreToolUse`,
126+> [!NOTE]
127+> Event names are case insensitive and snake-caseable, so `PreToolUse`,
67128> `pretooluse`, `PRETOOLUSE`, `pre_tool_use`, and `PRE_TOOL_USE` all work.
68129
69−## Baby's First Hook
70−
71−Hooks are just shell scripts. Go crazy.
130+**Scope**: `PreToolUse` only fires on the **top-level agent's** tool calls.
131+Sub-agents (the `agent` task tool, `agentic_fetch`, etc.) run without hook
132+interception so a single delegated turn doesn't trigger your hook N times. The
133+outer sub-agent tool call itself _is_ hooked, so policy like "never let the
134+agent spawn sub-agents" still works.
72135
73−```bash
74−#!/usr/bin/env bash
75−
76−# Log all bash tool calls to a file.
77−printf "%s: %s / %s" \
78− "$(date -Iseconds)" \
79− "$CRUSH_SESSION_ID" \
80− "$CRUSH_TOOL_INPUT_COMMAND" >> ./bash.log
81−```
82−
83−That's basically it. For the full guide on how hooks work, however, read on.
136+Hooks are keyed by event name. Only `command` is required, and you can omit
137+`matcher` to match all tools.
84138
85139## Building Hooks
86140
@@ -93,6 +147,11 @@ When a hook fires, Crush:
931474. Waits for all to finish (or time out), then aggregates results **in config
94148 order**: deny wins over allow, allow wins over none; `updated_input` patches
95149 shallow-merge in order.
150+5. Applies the result **before** permission checks. If the aggregated decision
151+ is `deny`, the tool call is blocked and you never see a permission prompt
152+ for it. If it's `allow`, Crush treats that as affirmative pre-approval and
153+ also skips the prompt. Silence (no decision) falls through to the normal
154+ permission flow.
96155
97156Note that you can omit `matcher` and match in your shell script instead, however
98157you'll incur some additional overhead as Crush will `exec` each script.
@@ -436,6 +495,20 @@ process.stdin.on("end", () => {
436495
437496---
438497
498+## Claude Code compatibility
499+
500+Crush hooks are broadly compatible with [Claude Code
501+hooks](https://docs.claude.com/en/docs/claude-code/hooks): the config shape,
502+stdin payload, output envelope, and exit codes line up so most Claude Code
503+hooks run under Crush unchanged. This document covers the Crush-specific API
504+only — anything not documented here isn't guaranteed to work.
505+
506+One intentional divergence: Crush treats `updated_input` as a shallow-merge
507+patch against the original `tool_input` rather than a full replacement. Keys
508+you omit are preserved. See [Output](#output) for details.
509+
510+---
511+
439512## Reference
440513
441514This is the official reference of the narrative above. If prose and this section