Crush Hooks
Add and debug hooks to gate, approve, or modify tool calls in Crush.
Installation
- Make sure Claude is on your device and in your terminal.
Skills load from
~/.claude/skills/when Claude Code starts up — so you need it on your machine first. If you don't have it yet, install it once with the command below, then runclaudein any terminal to verify.One-time setupnpm i -g @anthropic-ai/claude-codeAlready have it? Skip ahead.
- Paste into Claude Code or into your terminal.
This copies the whole skill folder into
~/.claude/skills/crush-hooks-charmbracelet/— the SKILL.md plus any scripts, reference docs, or templates the skill ships with. Safe default: works for every skill.Faster alternative (instruction-only skills)
Skips the clone and grabs only the SKILL.md file. Don't use this if the skill ships Python scripts, reference markdowns, or asset templates — they won't be downloaded and the skill will fail when it tries to load them.
Quick install (SKILL.md only)Sign up to copy - Restart Claude Code.
Quit and reopen Claude Code (or any other agent that loads from
~/.claude/skills/). New skills are picked up on startup. - Just ask Claude.
Skills auto-activate when your request matches the skill's description — no slash command needed. Trigger phrases live in the skill's own frontmatter; you can read them in the “What this skill does” section above.
Prefer to read the source first? Open on GitHub.
When Claude uses it
Use when the user wants to add, write, debug, or configure a Crush hook — gating or blocking tool calls, approving or rewriting tool input before execution, injecting context into tool results, or troubleshooting hook behavior in crush.json.
What this skill does
Crush Hooks
Hooks are user-defined commands in crush.json that fire at specific points
during execution, giving deterministic control over tool behavior. They run
before permission checks and only on the top-level agent's tool calls —
sub-agent calls (task tool, agentic_fetch, etc.) are not intercepted, though
the sub-agent tool call itself is.
For the full reference, see docs/hooks/README.md. This skill covers what you
need to author correct hooks.
Supported Events
Only PreToolUse is currently supported. Event names are case-insensitive and
accept snake_case (PreToolUse, pretooluse, pre_tool_use all work).
Configuration
{
"hooks": {
"PreToolUse": [
{
"matcher": "^bash$", // regex against tool name (optional; omit to match all)
"command": "./hooks/my-hook.sh", // required: shell command to run
"timeout": 10 // optional: seconds, default 30
}
]
}
}
Project-level hooks take precedence over global. Matching hooks are deduped by
command, run in parallel, and aggregated in config order (not finish order).
Language
command is a shell command, so hooks can be written in any language by
invoking the interpreter: node ./hooks/h.js, python3 ./hooks/h.py,
./hooks/h.sh, inline echo '…', etc. The rest of this skill shows bash, but
the input/output contract is identical regardless of language.
Input
Environment variables:
| Variable | Description |
|---|---|
CRUSH_EVENT | Event name (e.g. PreToolUse) |
CRUSH_TOOL_NAME | Tool being called (e.g. bash) |
CRUSH_SESSION_ID | Current session ID |
CRUSH_CWD | Working directory |
CRUSH_PROJECT_DIR | Project root directory |
CRUSH_TOOL_INPUT_COMMAND | For bash calls: the shell command |
CRUSH_TOOL_INPUT_FILE_PATH | For file tools: the target file path |
JSON on stdin:
{
"event": "PreToolUse",
"session_id": "313909e",
"cwd": "/home/user/project",
"tool_name": "bash",
"tool_input": {"command": "rm -rf /"}
}
Output
Communicate back via exit code (+ stderr) or JSON on stdout.
| Exit Code | Meaning |
|---|---|
| 0 | Success. Stdout is parsed as the JSON envelope below. |
| 2 | Block this tool call. Stderr becomes the deny reason. |
| 49 | Halt the whole turn. Stderr becomes the halt reason. |
| Other | Non-blocking error. Logged and ignored; tool call proceeds. |
Exit 2 blocks one tool call (agent sees the reason and can try again); exit 49 ends the whole turn (user takes over). Default to deny — reach for halt only when letting the agent retry is itself the problem (e.g. secrets detected, policy violation).
JSON envelope (exit 0):
{
"version": 1,
"decision": "allow",
"halt": false,
"reason": "...",
"context": "Extra info for the model",
"updated_input": {"command": "rewritten"}
}
decision:"allow","deny", or omit."allow"is affirmative pre-approval — it bypasses the permission prompt entirely. Omit it (ornull) when you only want to inject context or rewrite input without also auto-approving the call.halt: true: ends the turn (same as exit 49).reason: shown to the model on deny; to model and user on halt.context: string or array of strings. Appended to what the model sees. Empty entries are dropped.updated_input: shallow-merge patch againsttool_input, not a replacement. Keys you include overwrite; keys you don't are preserved. Nested objects are replaced wholesale, not deep-merged. Ignored on deny/halt.
Aggregation (Multiple Hooks)
Composed in config order:
deny>allow> no opinion. First deny decides; subsequent allows don't override.haltis sticky: any hook halting ends the turn.reasonandcontextconcatenate in config order (newline-joined).updated_inputpatches shallow-merge sequentially; later patches win on colliding keys.
Canonical Examples
Block destructive commands
#!/usr/bin/env bash
set -euo pipefail
if echo "$CRUSH_TOOL_INPUT_COMMAND" | grep -qE 'rm\s+-(rf|fr)\s+/'; then
echo "Refusing to run rm -rf against root" >&2
exit 2
fi
Config: {"matcher": "^bash$", "command": "./hooks/no-rm-rf.sh"}
Auto-approve read-only tools (inline, no script)
{"matcher": "^(view|ls|grep|glob)$", "command": "echo '{\"decision\":\"allow\"}'"}
Every view/ls/grep/glob call now runs without prompting.
Inject context without auto-approving
Emit only context — omit decision so the normal permission flow still runs.
#!/usr/bin/env bash
set -euo pipefail
if [[ "$CRUSH_TOOL_INPUT_FILE_PATH" == *.go ]]; then
echo '{"context": "Remember: run gofumpt after editing Go files."}'
else
echo '{}'
fi
Config: {"matcher": "^(edit|write|multiedit)$", "command": "./hooks/go-context.sh"}
Rewrite tool input (shallow merge)
#!/usr/bin/env bash
set -euo pipefail
read -r input
rewritten=$(echo "$input" | jq -r '.tool_input.command' | some-rewriter)
cat <<EOF
{
"context": "Rewrote command",
"updated_input": {"command": "$rewritten"}
}
EOF
If the original call was {"command": "npm test", "timeout": 60000}, the
tool runs with {"command": "<rewritten>", "timeout": 60000} — timeout is
preserved.
Authoring Checklist
- Add
#!/usr/bin/env bashandset -euo pipefail(for shell scripts). chmod +xthe script.- Add the entry under
hooks.PreToolUseincrush.jsonwith the right matcher. - Decide intent: inject context (omit
decision), auto-approve ("allow"), block (exit 2), or halt (exit 49). - If rewriting input, remember
updated_inputis a shallow merge — only include the keys you want to change.
Debugging
- Timeouts kill the hook silently and the tool call proceeds. Bump
timeoutif needed. - Non-zero exit codes other than 2/49 are logged but don't block — check Crush logs.
- Use
echo "debug info" >&2for logging without corrupting stdout JSON. matcheris a regex against the tool name. Use^bash$(notbash) if you don't also want to matchmcp_something_bash.
Claude Code Compatibility
Crush also accepts Claude Code's hookSpecificOutput envelope. One intentional
divergence: Crush treats updated_input as shallow-merge, Claude Code replaces.
Existing Claude Code hooks work without modification for the matcher/decision
parts; revisit any that relied on updatedInput fully replacing tool input.
Related skills
Generative Code Art
anthropics
Create algorithmic art with p5.js using randomness and interactive parameters.
Poster & Visual Design
anthropics
Create original posters and visual art in PNG and PDF formats.
Claude API Helper
anthropics
Build, debug, and optimize Claude API applications with caching and model migration support.
MCP Server Builder
anthropics
Build protocol servers that connect language models to external APIs and services.