Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:52:01 +08:00
commit 6d7255ec20
10 changed files with 1616 additions and 0 deletions

566
commands/create-hook.md Normal file
View File

@@ -0,0 +1,566 @@
---
description: Create and configure Claude Code hooks with reference documentation
argument-hint: [hook-type] [matcher] [command]
---
# /create-hook
## Purpose
Create and configure Claude Code hooks with reference documentation and interactive guidance.
## Contract
**Inputs:**
- `$1` — HOOK_TYPE (optional: PreToolUse, PostToolUse, UserPromptSubmit, Notification, Stop, SubagentStop, PreCompact, SessionStart, SessionEnd)
- `$2` — MATCHER (optional: tool name pattern, e.g., "Bash", "Edit|Write", "*")
- `$3` — COMMAND (optional: shell command to execute)
**Outputs:** `STATUS=<OK|FAIL> HOOK_FILE=<path>`
## Instructions
1. **Detect common use cases (hybrid approach):**
- Scan user's request for keywords: "eslint", "prettier", "format", "lint", "typescript", "test", "commit"
- If detected, ask: "I can set up a production-ready [TOOL] hook. Would you like to use the template or create a custom hook?"
- If template chosen: Generate external script file in `.claude/hooks/` + settings.json entry
- If custom or no match: Fall back to current workflow (steps 2-3)
2. **Determine hook configuration mode:**
- If no arguments provided: Show interactive menu of hook types with examples
- If HOOK_TYPE provided: Guide user through creating that specific hook
- If all arguments provided: Create hook directly
3. **Validate inputs:**
- HOOK_TYPE must be one of the valid hook events
- MATCHER should be a valid tool name or pattern
- COMMAND should be a valid shell command or path to external script
- For external scripts: Ensure `.claude/hooks/` directory exists
4. **Reference documentation:**
Review the Claude Code hooks documentation for best practices and examples:
**Hook Events:**
- `PreToolUse`: Runs before tool calls (can block them)
- `PostToolUse`: Runs after tool calls complete
- `UserPromptSubmit`: Runs when user submits a prompt
- `Notification`: Runs when Claude Code sends notifications
- `Stop`: Runs when Claude Code finishes responding
- `SubagentStop`: Runs when subagent tasks complete
- `PreCompact`: Runs before compact operations
- `SessionStart`: Runs when session starts/resumes
- `SessionEnd`: Runs when session ends
5. **Production-Ready Script Templates:**
When user requests common integrations, generate these external scripts in `.claude/hooks/`:
**ESLint Auto-Fix (`run-eslint.sh`):**
```bash
#!/usr/bin/env bash
# Auto-fix JavaScript/TypeScript files with ESLint after edits
set -euo pipefail
# Extract file path from Claude's JSON payload
file_path="$(jq -r '.tool_input.file_path // ""')"
# Only process JS/TS files
[[ "$file_path" =~ \.(js|jsx|ts|tsx)$ ]] || exit 0
# Auto-detect package manager (prefer project's lock file)
if command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
PM="pnpm exec"
elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
PM="yarn"
else
PM="npx"
fi
# Run ESLint with auto-fix from project root
cd "$CLAUDE_PROJECT_DIR" && $PM eslint --fix "$file_path"
```
**Prettier Format (`run-prettier.sh`):**
```bash
#!/usr/bin/env bash
# Format files with Prettier after edits
set -euo pipefail
file_path="$(jq -r '.tool_input.file_path // ""')"
# Skip non-formattable files
[[ "$file_path" =~ \.(js|jsx|ts|tsx|json|css|scss|md|html|yml|yaml)$ ]] || exit 0
# Auto-detect package manager
if command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
PM="pnpm exec"
elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
PM="yarn"
else
PM="npx"
fi
cd "$CLAUDE_PROJECT_DIR" && $PM prettier --write "$file_path"
```
**TypeScript Type Check (`run-typescript.sh`):**
```bash
#!/usr/bin/env bash
# Run TypeScript type checker on TS/TSX file edits
set -euo pipefail
file_path="$(jq -r '.tool_input.file_path // ""')"
# Only process TypeScript files
[[ "$file_path" =~ \.(ts|tsx)$ ]] || exit 0
# Auto-detect package manager
if command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
PM="pnpm exec"
elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
PM="yarn"
else
PM="npx"
fi
# Run tsc --noEmit to check types without emitting files
cd "$CLAUDE_PROJECT_DIR" && $PM tsc --noEmit --pretty
```
**Run Affected Tests (`run-tests.sh`):**
```bash
#!/usr/bin/env bash
# Run tests for modified files
set -euo pipefail
file_path="$(jq -r '.tool_input.file_path // ""')"
# Only run tests for source files (not test files themselves)
[[ "$file_path" =~ \.(test|spec)\.(js|ts|jsx|tsx)$ ]] && exit 0
[[ "$file_path" =~ \.(js|jsx|ts|tsx)$ ]] || exit 0
# Auto-detect test runner and package manager
if [ -f "vitest.config.ts" ] || [ -f "vitest.config.js" ]; then
TEST_CMD="vitest related --run"
elif [ -f "jest.config.js" ] || [ -f "jest.config.ts" ]; then
TEST_CMD="jest --findRelatedTests"
else
exit 0 # No test runner configured
fi
if command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
PM="pnpm exec"
elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
PM="yarn"
else
PM="npx"
fi
cd "$CLAUDE_PROJECT_DIR" && $PM $TEST_CMD "$file_path"
```
**Commit Message Validation (`validate-commit.sh`):**
```bash
#!/usr/bin/env bash
# Validate commit messages follow conventional commits format
set -euo pipefail
# Extract commit message from tool input
commit_msg="$(jq -r '.tool_input // ""')"
# Check for conventional commit format: type(scope): message
if ! echo "$commit_msg" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\(.+\))?: .+'; then
echo "ERROR: Commit message must follow conventional commits format"
echo "Expected: type(scope): description"
echo "Got: $commit_msg"
exit 2 # Block the commit
fi
```
**Corresponding settings.json entries:**
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"comment": "Auto-fix with ESLint",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-eslint.sh"
}
]
}
]
}
}
```
6. **Best Practices for Hook Development:**
**Always use shell safety headers:**
```bash
#!/usr/bin/env bash
set -euo pipefail # Exit on error, undefined vars, pipe failures
```
**Extract data from Claude's JSON payload:**
```bash
# File path (Edit/Write tools)
file_path="$(jq -r '.tool_input.file_path // ""')"
# Command (Bash tool)
command="$(jq -r '.tool_input.command // ""')"
# Tool name
tool_name="$(jq -r '.tool_name // ""')"
# Full tool input
tool_input="$(jq -r '.tool_input' | jq -c .)"
```
**Use environment variables provided by Claude:**
```bash
$CLAUDE_PROJECT_DIR # Project root directory
$CLAUDE_USER_DIR # User's ~/.claude directory
$CLAUDE_SESSION_ID # Current session identifier
```
**Efficient file extension filtering:**
```bash
# Good: Use bash regex matching
[[ "$file_path" =~ \.(ts|tsx)$ ]] || exit 0
# Avoid: Spawning grep subprocess
echo "$file_path" | grep -q '\.ts$' || exit 0
```
**Package manager auto-detection pattern:**
```bash
# Check lock files to match project's package manager
if command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
PM="pnpm exec"
elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
PM="yarn"
else
PM="npx" # Fallback to npx
fi
```
**Exit codes matter:**
```bash
exit 0 # Success: Allow operation to continue
exit 1 # Error: Log error but don't block
exit 2 # Block: Prevent operation in PreToolUse hooks
```
**Performance considerations:**
- Avoid heavy operations in tight loops (e.g., don't run full test suite on every file edit)
- Use file extension checks to skip irrelevant files early
- Consider async/background execution for slow operations
- Cache results when possible (e.g., dependency checks)
**When to use external scripts vs inline commands:**
- **External scripts** (`.claude/hooks/*.sh`): Complex logic, multiple steps, reusable patterns
- **Inline commands**: Simple one-liners, quick jq filters, logging
Example inline command:
```json
"command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
```
7. **Common use cases and examples (updated with best practices):**
**Logging Bash commands:**
```json
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"comment": "Log all Bash commands with descriptions",
"command": "set -euo pipefail; cmd=$(jq -r '.tool_input.command // \"\"'); desc=$(jq -r '.tool_input.description // \"No description\"'); echo \"[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $cmd - $desc\" >> \"$CLAUDE_USER_DIR/bash-command-log.txt\""
}]
}]
}
}
```
**Auto-format TypeScript files with Prettier:**
```json
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"comment": "Auto-format TypeScript files after edits",
"command": "set -euo pipefail; file_path=$(jq -r '.tool_input.file_path // \"\"'); [[ \"$file_path\" =~ \\.(ts|tsx)$ ]] || exit 0; cd \"$CLAUDE_PROJECT_DIR\" && npx prettier --write \"$file_path\""
}]
}]
}
}
```
**Block sensitive file edits:**
```json
{
"hooks": {
"PreToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"comment": "Prevent edits to sensitive files",
"command": "set -euo pipefail; file_path=$(jq -r '.tool_input.file_path // \"\"'); if [[ \"$file_path\" =~ \\.(env|secrets|credentials) ]] || [[ \"$file_path\" == *\"package-lock.json\" ]] || [[ \"$file_path\" == *.git/* ]]; then echo \"ERROR: Cannot edit sensitive file: $file_path\" >&2; exit 2; fi"
}]
}]
}
}
```
**Desktop notifications:**
```json
{
"hooks": {
"Notification": [{
"matcher": "*",
"hooks": [{
"type": "command",
"comment": "Send desktop notifications",
"command": "if command -v notify-send >/dev/null 2>&1; then notify-send 'Claude Code' 'Awaiting your input'; elif command -v osascript >/dev/null 2>&1; then osascript -e 'display notification \"Awaiting your input\" with title \"Claude Code\"'; fi"
}]
}]
}
}
```
8. **Generic Patterns Reference:**
**Pattern: Extract file path and check multiple extensions**
```bash
set -euo pipefail
file_path=$(jq -r '.tool_input.file_path // ""')
[[ "$file_path" =~ \.(js|ts|jsx|tsx|py|go|rs)$ ]] || exit 0
# Your command here
```
**Pattern: Process multiple files from array**
```bash
set -euo pipefail
jq -r '.tool_input.files[]?' | while IFS= read -r file; do
[[ -f "$file" ]] && echo "Processing: $file"
# Your processing logic here
done
```
**Pattern: Conditional execution based on directory**
```bash
set -euo pipefail
file_path=$(jq -r '.tool_input.file_path // ""')
# Only process files in src/ directory
[[ "$file_path" =~ ^src/ ]] || exit 0
# Your command here
```
**Pattern: Extract and validate Bash command**
```bash
set -euo pipefail
cmd=$(jq -r '.tool_input.command // ""')
# Block dangerous commands
if [[ "$cmd" =~ (rm -rf|mkfs|dd|:(){:|:&};:) ]]; then
echo "ERROR: Dangerous command blocked" >&2
exit 2
fi
```
**Pattern: Background/async execution**
```bash
set -euo pipefail
file_path=$(jq -r '.tool_input.file_path // ""')
# Run slow operation in background, don't block Claude
(cd "$CLAUDE_PROJECT_DIR" && npm run build "$file_path" &> /tmp/build.log) &
```
**Pattern: Conditional tool execution**
```bash
set -euo pipefail
tool_name=$(jq -r '.tool_name // ""')
case "$tool_name" in
Edit|Write)
file_path=$(jq -r '.tool_input.file_path // ""')
echo "File modified: $file_path"
;;
Bash)
cmd=$(jq -r '.tool_input.command // ""')
echo "Command executed: $cmd"
;;
esac
```
**Pattern: Multi-tool chain with error handling**
```bash
set -euo pipefail
file_path=$(jq -r '.tool_input.file_path // ""')
[[ "$file_path" =~ \.(ts|tsx)$ ]] || exit 0
cd "$CLAUDE_PROJECT_DIR"
# Run linter
if ! npx eslint --fix "$file_path" 2>/dev/null; then
echo "Warning: ESLint failed" >&2
fi
# Run formatter (always runs even if linter fails)
npx prettier --write "$file_path" 2>/dev/null || true
```
**Pattern: Cache validation results**
```bash
set -euo pipefail
file_path=$(jq -r '.tool_input.file_path // ""')
cache_file="/tmp/claude-hook-cache-$(echo \"$file_path\" | md5sum | cut -d' ' -f1)"
# Check cache freshness
if [[ -f "$cache_file" ]] && [[ "$cache_file" -nt "$file_path" ]]; then
cat "$cache_file"
exit 0
fi
# Run expensive validation
result=$(npx tsc --noEmit "$file_path" 2>&1)
echo "$result" > "$cache_file"
echo "$result"
```
**Pattern: Cross-platform compatibility**
```bash
set -euo pipefail
# Detect OS and use appropriate commands
case "$(uname -s)" in
Darwin*)
# macOS
osascript -e 'display notification "Build complete"'
;;
Linux*)
# Linux
notify-send "Build complete"
;;
MINGW*|MSYS*|CYGWIN*)
# Windows
powershell -Command "New-BurntToastNotification -Text 'Build complete'"
;;
esac
```
9. **Security considerations:**
- Hooks run automatically with your environment's credentials
- Always review hook implementation before registering
- Be cautious with hooks that execute external commands
- Avoid hardcoding sensitive data in hook commands
- Use exit code 2 to block operations in PreToolUse hooks
10. **Hook creation workflow:**
**For common tools (ESLint, Prettier, TypeScript, Tests):**
1. Detect tool name from user request keywords
2. Ask user: "I can set up a production-ready [TOOL] hook. Use template or create custom?"
3. If template:
- Create `.claude/hooks/` directory if needed
- Generate appropriate script file (e.g., `run-eslint.sh`)
- Make script executable (`chmod +x`)
- Add settings.json entry with `$CLAUDE_PROJECT_DIR` reference
- Verify package manager and dependencies exist
4. Provide setup summary and next steps
**For custom hooks:**
1. Determine appropriate hook event for the use case
2. Define matcher pattern (specific tool name, regex like "Edit|Write", or "*" for all)
3. Write shell command that processes JSON input via stdin
4. Always start with `set -euo pipefail` for safety
5. Use `$CLAUDE_PROJECT_DIR` and other environment variables
6. Test hook command independently before registering
7. Choose storage location:
- `.claude/settings.json` - Project-specific, committed to git
- `.claude/settings.local.json` - Project-specific, not committed
- `~/.claude/settings.json` - User-wide, all projects
8. Register hook and reload with `/hooks` command
11. **Debugging hooks:**
- Run `claude --debug` to see hook execution logs
- Use `/hooks` command to verify hook registration
- Check hook configuration in settings files
- Test hook command standalone with sample JSON:
```bash
echo '{"tool_name":"Edit","tool_input":{"file_path":"test.ts"}}' | .claude/hooks/run-eslint.sh
```
- Review Claude Code output for hook execution errors
- Use `jq` to inspect JSON data passed to hooks
- Check exit codes (0=success/continue, 1=error/log, 2=block operation)
- Verify file permissions (`chmod +x` for external scripts)
- Check that `$CLAUDE_PROJECT_DIR` resolves correctly
12. **Output:**
**Template mode:**
- Create external script file in `.claude/hooks/[script-name].sh`
- Generate settings.json configuration snippet
- Make script executable
- Print: `STATUS=OK HOOK_FILE=.claude/hooks/[script-name].sh SETTINGS=.claude/settings.json`
- Show: Next steps (reload hooks, test the hook)
**Interactive/Direct mode:**
- Guide user through hook creation with prompts
- Show the complete JSON configuration to add
- Provide instructions for registering the hook
- Print: `STATUS=OK HOOK_FILE=~/.claude/settings.json` or `STATUS=OK HOOK_FILE=.claude/settings.local.json`
- Remind user to run `/hooks` to reload configuration
## Constraints
- External scripts must be executable (`chmod +x`)
- Hooks must be valid shell commands or script paths
- JSON structure must follow hooks schema
- PreToolUse hooks can block operations with exit code 2
- Hooks should be idempotent and handle errors gracefully
- Consider performance impact of hooks in tight loops
- Always use `set -euo pipefail` in bash scripts for safety
- Use `$CLAUDE_PROJECT_DIR` for project-relative paths
- Test hooks standalone before deploying
## Examples
**Template mode (recommended for common tools):**
```bash
# User: "Set up ESLint to run on file edits"
/create-hook
# Detects "ESLint", offers template
# → Creates .claude/hooks/run-eslint.sh
# → Adds PostToolUse hook to settings.json
# → Makes script executable
# → STATUS=OK HOOK_FILE=.claude/hooks/run-eslint.sh
```
**Interactive mode:**
```bash
/create-hook
# Shows menu of:
# 1. Common tools (ESLint, Prettier, TypeScript, Tests)
# 2. Hook types (PreToolUse, PostToolUse, etc.)
# 3. Custom hook creation
```
**Guided mode:**
```bash
/create-hook PostToolUse
# Guides through creating a PostToolUse hook
# Asks for: matcher, command/script, storage location
```
**Direct mode:**
```bash
/create-hook PreToolUse "Bash" "set -euo pipefail; cmd=\$(jq -r '.tool_input.command'); echo \"[\$(date -Iseconds)] \$cmd\" >> \"\$CLAUDE_USER_DIR/bash.log\""
# Creates hook configuration directly with best practices
```
## Reference
For complete documentation, see: https://docs.claude.com/en/hooks