on main help sign in
docs(hooks): improve hook documentation
e5611c3Christian Rocha
docs/hooks/FUTURE.md+2−1
@@ -4,7 +4,8 @@ This document tracks planned features and design notes for hooks that are not
4yet implemented. Nothing here is part of the current contract. Treat it as a
4yet implemented. Nothing here is part of the current contract. Treat it as a
5scratchpad for what's next, not as documentation of current behavior.
5scratchpad for what's next, not as documentation of current behavior.
6
6
7+> [!NOTE]
7−> [!NOTE] This document was largely LLM-generated.
8+> This document was largely LLM-generated.
8
9
9## `context_files`
10## `context_files`
10
11
docs/hooks/README.md+115−42
@@ -1,38 +1,97 @@
1# Hooks
1# Hooks
2
2
3+> [!NOTE]
3−> [!NOTE] This document was designed for both humans and agents.
4+> This document was designed for both humans and agents.
4
5
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.
7−behavior.
8
9
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.
13
12
14### Hot Hook Facts
13### Hot Hook Facts
15
14
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
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+---
31
90
32## Configuration
91## Configuration
33
92
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.
36
95
37```jsonc
96```jsonc
38{
97{
@@ -48,8 +107,8 @@ with project level hooks taking precedence.
48}
107}
49```
108```
50
109
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.
53
112
54## Events
113## Events
55
114
@@ -58,29 +117,24 @@ Here are the events you can hook into (spoiler: there's currently just one):
58### PreToolUse
117### PreToolUse
59
118
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
121+stuff, and so on.
62
122
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`).
65
125
126+> [!NOTE]
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.
68
129
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.
72
135
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.
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.
84
138
85## Building Hooks
139## Building Hooks
86
140
@@ -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
154+ permission flow.
96
155
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", () => {
436
495
437---
496---
438
497
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+
439## Reference
512## Reference
440
513
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