Crush — Hooks
- File format
- JSON
- Discovery path
crush.json- Syllago install method
- JSON merge
- Symlink support
- No
- Config file
crush.json- Handler types
command
Hook events
| Canonical Event | Native Name | Category |
|---|---|---|
before_tool_execute | PreToolUse | Tool |
Features
How each feature converts to syllago's canonical format. See format conversion for what these statuses mean.
Fields
Native frontmatter or config fields this provider exposes. The first column is the provider's own field name; the canonical key column links to syllago's cross-provider equivalent.
| Provider field | Conversion | Canonical key | Summary |
|---|---|---|---|
context | TranslatedConversion type: TranslatedActively mapped to the target provider’s equivalent field during conversion.Learn more → | context_injection | The output envelope's context field accepts a string or an array of strings, appended to what the model sees for the current turn. Across multiple hooks, context values concatenate in config order; empty entries are dropped. |
decision | TranslatedConversion type: TranslatedActively mapped to the target provider’s equivalent field during conversion.Learn more → | decision_control | Exit code 2 blocks the tool call (stderr becomes the deny reason); exit 49 halts the whole turn; other non-zero exit codes are non-blocking errors and the tool call proceeds. On exit 0, stdout is parsed as a JSON envelope whose decision field may be allow, deny, or null. allow is affirmative pre-approval (skips the permission prompt); null/omitted means no opinion. Across multiple hooks, deny > allow > null, composed in config order. |
hooks | TranslatedConversion type: TranslatedActively mapped to the target provider’s equivalent field during conversion.Learn more → | — | The hooks config key is an object keyed by event name, with each value an array of HookConfig objects — multiple hooks per event with per-tool matcher filtering. Event names are case insensitive and snake-caseable: PreToolUse, pretooluse, and pre_tool_use all work. |
matcher | TranslatedConversion type: TranslatedActively mapped to the target provider’s equivalent field during conversion.Learn more → | matcher_patterns | HookConfig has an optional matcher field: a regex tested against the tool name (e.g. bash, edit, mcp_github_create_pull_request). Omitted or empty matcher matches all tools. |
name | TranslatedConversion type: TranslatedActively mapped to the target provider’s equivalent field during conversion.Learn more → | — | HookConfig has an optional name field: a friendly display name shown in the TUI, falling back to the command when omitted. Maps from the canonical hook manifest's name field on install. |
updated_input | TranslatedConversion type: TranslatedActively mapped to the target provider’s equivalent field during conversion.Learn more → | input_modification | The exit-0 JSON envelope may include updated_input: a shallow-merge patch against tool_input. Included keys overwrite matching keys; omitted keys are preserved; nested objects are replaced wholesale. Patches from multiple hooks shallow-merge sequentially in config order (later hooks win on colliding keys) and are ignored if the final decision is deny or halt. Implemented as UpdatedInput in internal/hooks/hooks.go. |
timeout | Not portableConversion type: Not portableUnique to this provider — can’t be carried across providers.Learn more → | — | Each HookConfig entry has a timeout field (integer, seconds, default 30). On timeout, Crush cancels the hook's context, waits a short grace period, then treats the result as a non-blocking error so the tool call proceeds — a timed-out hook cannot block. |
Other features
Behaviors, conventions, and capabilities that aren't tied to a single named field — things like path-based activation, discovery rules, and lifecycle behavior.
| Feature | Conversion | Summary |
|---|---|---|
hook_scopes | TranslatedConversion type: TranslatedActively mapped to the target provider’s equivalent field during conversion.Learn more → | Hooks can be configured in crush.json (or .crush.json) at both the user-global (~/.config/crush/crush.json) and project level, with project-level hooks taking precedence. Global-config commands must use absolute paths or inline commands; relative paths resolve against the working directory. |
json_io_protocol | TranslatedConversion type: TranslatedActively mapped to the target provider’s equivalent field during conversion.Learn more → | Stdin provides a JSON payload: event, session_id, cwd, plus tool_name and tool_input (the raw JSON the model sent) for PreToolUse. On exit 0 stdout is parsed as a JSON output envelope: version (defaults to 1), decision, halt, reason, context, updated_input. Simple hooks can skip JSON entirely and use CRUSH_* environment variables plus exit codes. |
permission_control | TranslatedConversion type: TranslatedActively mapped to the target provider’s equivalent field during conversion.Learn more → | Hook results apply before permission checks. An aggregated deny blocks the tool call before any permission prompt; an aggregated allow is affirmative pre-approval that skips the prompt entirely; silence (no decision) falls through to the normal permission flow. |
| Environment-variable input | Not portableConversion type: Not portableUnique to this provider — can’t be carried across providers.Learn more → | Alongside stdin JSON, every hook receives environment variables: CRUSH=1, AGENT=crush, AI_AGENT=crush, CRUSH_EVENT, CRUSH_TOOL_NAME, CRUSH_SESSION_ID, CRUSH_CWD, CRUSH_PROJECT_DIR, and per-tool shortcuts CRUSH_TOOL_INPUT_COMMAND (bash) and CRUSH_TOOL_INPUT_FILE_PATH (file tools) — simple hooks never need to parse JSON. |
| Hooks fire before permission checks | Not portableConversion type: Not portableUnique to this provider — can’t be carried across providers.Learn more → | Hooks run before Crush's built-in permission gate: deny blocks the tool call before any prompt appears, and allow is affirmative pre-approval that skips the prompt — hooks can both veto and auto-approve independent of configured permissions. |
| Parallel execution, config-order composition | Not portableConversion type: Not portableUnique to this provider — can’t be carried across providers.Learn more → | Matching hooks run in parallel for speed, deduplicated by identical command, and their results compose deterministically in config order: deny wins over allow, reasons and context concatenate in order, updated_input patches merge in order. Timing never determines the outcome. |
| Top-level-agent-only scope | Not portableConversion type: Not portableUnique to this provider — can’t be carried across providers.Learn more → | PreToolUse fires only on the top-level agent's tool calls; sub-agents (the agent task tool, agentic_fetch) run without hook interception so one delegated turn doesn't fire a hook N times. The outer sub-agent tool call itself is hooked. |
| Turn halt (exit 49 / halt field) | Not portableConversion type: Not portableUnique to this provider — can’t be carried across providers.Learn more → | A hook can end the whole turn, not just block one tool call: exit code 49 (stderr becomes the halt reason) or halt: true in the JSON envelope. The agent doesn't respond further; the user takes over. Halt is sticky across multiple hooks. |