@@ -29,6 +29,45 @@ languages at the end, too.
29- Crush currently supports just one hook, `PreToolUse`, with plans to support
29- 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.
30 the full gamut. If there's a hook you'd like to see, let us know.
34+Hooks run through Crush's embedded POSIX shell (`mvdan.cc/sh`) — the same
35+interpreter the `bash` tool uses. Inline commands and shebang-less scripts
36+execute in-process; scripts with a `#!` shebang dispatch to the named
37+interpreter via `os/exec`. This contract is identical on macOS, Linux, and
40+What this means in practice:
42+- **Windows without Unix tooling**: inline shell (`echo`, pipelines, `jq`,
43+ `grep`), shebang-less `.sh` scripts, inline PowerShell
44+ (`powershell -Command …`), and `.exe` invocations all work out of the box
45+ with no WSL, Git Bash, Cygwin, or MSYS required.
46+- **PowerShell scripts** (`.ps1`) are not auto-dispatched by extension.
47+ Invoke them explicitly: `powershell -File ./audit.ps1` (or
48+ `pwsh -File ./audit.ps1`).
49+- **Shebang'd scripts** require the named interpreter on `PATH`. Git for
50+ Windows ships `bash.exe`, which makes `#!/bin/bash` and
51+ `#!/usr/bin/env bash` scripts work on Windows the same way they do on
52+ Unix. CRLF line endings in the shebang line are tolerated.
53+- **Permissive shebang fallback**: if the absolute path in a shebang
54+ doesn't exist (e.g. `#!/bin/bash` on Windows), Crush falls back to a
55+ `PATH` lookup of the base name (`bash`) before giving up. A debug-level
56+ log records the fallback. If the interpreter isn't on `PATH` either, the
57+ hook fails cleanly as a non-blocking warning and the agent proceeds as
59+- **Environment**: every hook sees `CRUSH=1`, `AGENT=crush`, and
60+ `AI_AGENT=crush` on top of the `CRUSH_*` hook-specific variables. These
61+ three markers are guaranteed and match what the `bash` tool sets, so
62+ scripts that detect "am I being run by an AI agent?" behave the same in
64+- **Timeout behavior**: when a hook exceeds its timeout, Crush cancels the
65+ context and waits a short grace period (~1s) for the interpreter to
66+ yield. If the hook still hasn't returned, Crush abandons it, logs a
67+ warning, and treats the result as "no opinion" so the agent can proceed.
68+ Long-running work should honor context cancellation or run in a
69+ subprocess via a shebang.
32## Configuration
71## Configuration
34Hooks can be added to your `crush.json` at both the global and project-level,
73Hooks can be added to your `crush.json` at both the global and project-level,
@@ -89,13 +128,15 @@ When a hook fires, Crush:
891. Filters hooks whose `matcher` regex matches the tool name (no matcher = match
1281. Filters hooks whose `matcher` regex matches the tool name (no matcher = match
912. Deduplicates by `command` (identical commands run once).
1302. Deduplicates by `command` (identical commands run once).
92−3. Runs all matching hooks **in parallel** as subprocesses.
131+3. Runs all matching hooks **in parallel** through Crush's embedded POSIX
132+ shell (see [Execution model](#execution-model)).
934. Waits for all to finish (or time out), then aggregates results **in config
1334. 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
134 order**: deny wins over allow, allow wins over none; `updated_input` patches
95 shallow-merge in order.
135 shallow-merge in order.
97−Note that you can omit `matcher` and match in your shell script instead, however
137+Note that you can omit `matcher` and match in your shell script instead,
98−you'll incur some additional overhead as Crush will `exec` each script.
138+however you'll incur some additional overhead as Crush will still parse and
@@ -109,6 +150,9 @@ The available environment variables are:
110| Variable | Description |
151| Variable | Description |
111| ---------------------------- | ---------------------------------------------- |
152| ---------------------------- | ---------------------------------------------- |
153+| `CRUSH` | Always `1` when running under Crush. |
154+| `AGENT` | Always `crush`. |
155+| `AI_AGENT` | Always `crush`. |
112| `CRUSH_EVENT` | The hook event name (e.g. `PreToolUse`). |
156| `CRUSH_EVENT` | The hook event name (e.g. `PreToolUse`). |
113| `CRUSH_TOOL_NAME` | The tool being called (e.g. `bash`). |
157| `CRUSH_TOOL_NAME` | The tool being called (e.g. `bash`). |
114| `CRUSH_SESSION_ID` | Current session ID. |
158| `CRUSH_SESSION_ID` | Current session ID. |
@@ -117,6 +161,10 @@ The available environment variables are:
117| `CRUSH_TOOL_INPUT_COMMAND` | For `bash` calls: the shell command being run. |
161| `CRUSH_TOOL_INPUT_COMMAND` | For `bash` calls: the shell command being run. |
118| `CRUSH_TOOL_INPUT_FILE_PATH` | For file tools: the target file path. |
162| `CRUSH_TOOL_INPUT_FILE_PATH` | For file tools: the target file path. |
164+The `CRUSH`, `AGENT`, and `AI_AGENT` markers are also set by the `bash`
165+tool, so a script can detect "am I running under Crush?" the same way in
122Standard input provides the full context as JSON:
170Standard input provides the full context as JSON:
@@ -267,9 +315,12 @@ When multiple hooks match the same tool call:
268### Timeouts
316### Timeouts
270−If a hook exceeds its timeout, the process is killed and treated as a
318+If a hook exceeds its timeout, Crush cancels its context and treats the
271−non-blocking error and the tool call proceeds. The default timeout is 30
319+result as a non-blocking error so the tool call proceeds. The default
272−seconds.
320+timeout is 30 seconds. Shebang-dispatched subprocesses are killed through
321+`exec.CommandContext`; in-process hooks get a short grace period to yield
322+and are then abandoned (the agent moves on regardless). Long-running work
323+should honor context cancellation or run out-of-process via a shebang.
274## Examples
325## Examples