@@ -1,38 +1,97 @@
3−> [!NOTE] This document was designed for both humans and agents.
4+> This document was designed for both humans and agents.
5−Hooks are user-defined shell commands that fire at specific points during
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
6−Crush's execution, giving you deterministic control over an agent's wily
8+its behavior, and exert deterministic control over an agent's wily behavior.
9−Hooks in Crush are shell-based, with a focus on simplicity. This allows hooks to
10+Hooks are just shell commands, and were designed to be both simple and future
10−effectively be written in any language. In this document we'll primarily focus
11+forward.
11−on Bash for simplicity's sake, though we'll include some examples in other
12−languages at the end, too.
14### Hot Hook Facts
13### Hot Hook Facts
16−- Hooks run before permission checks. If a hook denies a tool call, you'll
15+- Hooks just shell commands
17− never see a permission prompt for it. If a hook explicitly allows a tool
16+- Hooks can be written in any language because they’re just executables: Bash, Python, Node, Rust, Haskell, whatever
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
17+- Hooks are Claude Code-compatible
26− covers the Crush-specific API only. One intentional divergence: Crush treats
18+- Crush ships with a builtin `crush-hook` skill write, edit, and configure
27− `updated_input` as a shallow-merge patch rather than a full replacement — see
19+ hooks; just tell Crush how to configure Crush
28− [Output](#output) below.
29- Crush currently supports just one hook, `PreToolUse`, with plans to support
20- 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
25+### Some things you can do with hooks:
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
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
35+- Log certain tool calls
37+…And lots more. Show us what you're building!
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).
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.
51+ // As expected, hooks go in a "hooks" object.
53+ // PreToolUse is an event that fires before a tool is used.
56+ // What tool do we want to hook into? In this case, Bash, because it
57+ // runs the stuff we wanna block.
60+ // The path to our actual hook script.
61+ "command": "./no-haskell.sh",
68+Now, let's make our `no-haskell.sh` hook script.
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
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
82+ # Now, block the tool call by exiting with code 2.
87+That's basically it. For the full guide on how hooks work, however, read on.
32## Configuration
91## Configuration
34−Hooks can be added to your `crush.json` at both the global and project-level,
93+Hooks can be added to your `crush.json` (or `.crush.json`) at both the global
35−with project level hooks taking precedence.
94+and project-level, with project level hooks taking precedence.
@@ -48,8 +107,8 @@ with project level hooks taking precedence.
51−Hooks are keyed by event name. Only `command` is required; omit `matcher` to
110+Remember, hooks will run in parallel but resolve in config order. Last hook
52−match all tools.
111+wins when rewriting input, but first deny wins when blocking.
@@ -58,29 +117,24 @@ Here are the events you can hook into (spoiler: there's currently just one):
58### PreToolUse
117### PreToolUse
60This hook fires before every tool call. Use it to block dangerous commands,
119This 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
63**Matched against**: the tool name (e.g. `bash`, `edit`, `write`,
123**Matched against**: the tool name (e.g. `bash`, `edit`, `write`,
64`mcp_github_create_pull_request`).
124`mcp_github_create_pull_request`).
66−> [!NOTE] Event names are case insensitive and snake-caseable, so `PreToolUse`,
127+> Event names are case insensitive and snake-caseable, so `PreToolUse`,
67> `pretooluse`, `PRETOOLUSE`, `pre_tool_use`, and `PRE_TOOL_USE` all work.
128> `pretooluse`, `PRETOOLUSE`, `pre_tool_use`, and `PRE_TOOL_USE` all work.
69−## Baby's First Hook
130+**Scope**: `PreToolUse` only fires on the **top-level agent's** tool calls.
70−
131+Sub-agents (the `agent` task tool, `agentic_fetch`, etc.) run without hook
71−Hooks are just shell scripts. Go crazy.
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.
73−```bash
136+Hooks are keyed by event name. Only `command` is required, and you can omit
74−#!/usr/bin/env bash
137+`matcher` to match all tools.
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
83−That's basically it. For the full guide on how hooks work, however, read on.
85## Building Hooks
139## Building Hooks
@@ -93,6 +147,11 @@ When a hook fires, Crush:
934. Waits for all to finish (or time out), then aggregates results **in config
1474. Waits for all to finish (or time out), then aggregates results **in config
94 order**: deny wins over allow, allow wins over none; `updated_input` patches
148 order**: deny wins over allow, allow wins over none; `updated_input` patches
95 shallow-merge in order.
149 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
97Note that you can omit `matcher` and match in your shell script instead, however
156Note that you can omit `matcher` and match in your shell script instead, however
98you'll incur some additional overhead as Crush will `exec` each script.
157you'll incur some additional overhead as Crush will `exec` each script.
@@ -436,6 +495,20 @@ process.stdin.on("end", () => {
498+## Claude Code compatibility
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.
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.
439## Reference
512## Reference
441This is the official reference of the narrative above. If prose and this section
514This is the official reference of the narrative above. If prose and this section