Awesome-jetbrains-claude claude-code-hooks
Create custom Claude Code hooks — bash scripts (.sh) that execute automatically in response to Claude Code events (file edits, tool use, session lifecycle). Use when the user asks to create a hook, automation script for Claude Code, pre/post tool use check, or guard for Claude Code actions. Covers writing .sh files, registering hooks in settings.local.json, proper stdin/stdout/exit code handling, and all event types (PreToolUse, PostToolUse, SessionStart, Stop, etc.). Also use when debugging or fixing existing hooks.
git clone https://github.com/IliyaBrook/awesome-jetbrains-claude
T=$(mktemp -d) && git clone --depth=1 https://github.com/IliyaBrook/awesome-jetbrains-claude "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/claude-code-hooks" ~/.claude/skills/iliyabrook-awesome-jetbrains-claude-claude-code-hooks && rm -rf "$T"
.claude/skills/claude-code-hooks/SKILL.mdClaude Code Hooks
Create and register bash hook scripts for Claude Code automation.
Workflow
1. Clarify Requirements
Determine:
- What event? PreToolUse (validate/block before action), PostToolUse (check after action), SessionStart, Stop, etc.
- Which tools? Edit, Write, Bash, etc. — this becomes the
regexmatcher - What logic? What the script should check or enforce
- What feedback? Block the action, warn Claude, or just log
2. Write the .sh Script
Place in
.claude/hooks/<name>.sh. Follow the mandatory skeleton — every hook MUST include these elements in order:
#!/usr/bin/env bash set -uo pipefail # 1. Terminal guard (MANDATORY — prevents hang on manual run) if [[ -t 0 ]]; then exit 0 fi # 2. Read stdin ONCE (stdin is consumed on first read) input=$(cat) # 3. Extract fields with // empty (NEVER omit — prevents "null" strings) tool_name=$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null) file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null) # 4. Early exits (silent exit 0 = Claude ignores it) [ -z "$file_path" ] && exit 0 # 5. Make path absolute [[ "$file_path" = /* ]] || file_path="$(pwd)/$file_path" # 6. Main logic + output
3. Output Correctly
To send feedback to Claude (the ONLY correct way):
# Via jq (safe — handles special chars) printf '%s\n' "$reason" | jq -Rs '{"decision": "block", "reason": .}' exit 0 # Direct JSON (only for static messages) printf '%s' '{"decision": "block", "reason": "Error message"}' exit 0
For info messages (Claude sees as a note):
echo "Check passed" exit 0
NEVER do this:
echo "Error" >&2; exit 1 # Claude sees "hook error", NOT your message
4. Register in settings.local.json
Add to
.claude/settings.local.json (create if absent):
{ "hooks": { "<EventType>": [ { "matcher": "<tool_regex>", "hooks": [ { "type": "command", "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/<name>.sh\"", "timeout": 30, "statusMessage": "Running check..." } ] } ] } }
Always quote the path with escaped double quotes — spaces in path break execution.
5. Set Executable Permission
chmod +x .claude/hooks/<name>.sh
Critical Rules
— neverset -uo pipefail
(too aggressive for hooks)set -e
— MUST be first check, prevents hang[[ -t 0 ]] && exit 0
once — stdin is consumed on first read, cannot re-readinput=$(cat)
in every jq call — without it, missing fields return literal// empty"null"
notprintf '%s'
— echo mishandles special chars in JSONecho- JSON stdout + exit 0 — the ONLY way to send feedback to Claude
- exit 1 = hook failure — Claude ignores your message, logs it as error
- exit 2 = block — only works in PreToolUse
Reference
For complete documentation on event types, matchers, input JSON structure, all exit codes, and a real-world ts-diagnostics.sh example, read references/hooks-guide.md.