Initial commit
This commit is contained in:
189
hooks/README.md
Normal file
189
hooks/README.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# Claude Code Hooks
|
||||
|
||||
This directory contains hooks that integrate with Claude Code to provide real-time code quality validation.
|
||||
|
||||
## Available Hooks
|
||||
|
||||
### typescript-check.sh
|
||||
|
||||
Validates TypeScript files when they are created or modified through Claude Code's Write or Edit tools.
|
||||
|
||||
**What it checks:**
|
||||
|
||||
- ✅ TypeScript type errors (`tsc --noEmit`)
|
||||
- ✅ Biome lint/format errors (`biome check`)
|
||||
|
||||
**When it runs:**
|
||||
|
||||
- After Write tool creates a `.ts` or `.tsx` file
|
||||
- After Edit tool modifies a `.ts` or `.tsx` file
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- ✅ **Passes**: Allows the operation to complete
|
||||
- ❌ **Fails**: Blocks the operation and shows detailed errors with suggestions
|
||||
|
||||
## Hook Configuration
|
||||
|
||||
The `hooks.json` file registers hooks with Claude Code:
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "TypeScript and code quality validation hook for Write/Edit operations on .ts/.tsx files",
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/typescript-check.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **User requests file creation/edit**: Claude Code invokes Write or Edit tool
|
||||
2. **Tool completes**: File is written/edited to disk
|
||||
3. **Hook triggers**: `typescript-check.sh` executes
|
||||
4. **Validation runs**:
|
||||
- Checks if file is `.ts` or `.tsx`
|
||||
- Runs `tsc --noEmit` for type checking
|
||||
- Runs `biome check` for linting
|
||||
5. **Result**:
|
||||
- **All pass**: Operation succeeds, user sees success message
|
||||
- **Errors found**: Operation is blocked, user sees detailed error report
|
||||
|
||||
## Example Output
|
||||
|
||||
### ✅ Success
|
||||
|
||||
```
|
||||
TypeScript Check:
|
||||
✅ No type errors found
|
||||
|
||||
Biome Check:
|
||||
✅ No lint errors found
|
||||
|
||||
✅ Code quality checks passed!
|
||||
```
|
||||
|
||||
### ❌ Failure
|
||||
|
||||
```
|
||||
❌ Code quality checks failed for src/utils/helper.ts:
|
||||
|
||||
❌ TypeScript errors found:
|
||||
src/utils/helper.ts:15:3 - Type 'string' is not assignable to type 'number'
|
||||
src/utils/helper.ts:23:7 - Property 'email' does not exist on type 'User'
|
||||
|
||||
❌ Biome lint/format errors found:
|
||||
src/utils/helper.ts:15:3 - Unused variable 'response'
|
||||
src/utils/helper.ts:42:1 - Missing return type
|
||||
|
||||
💡 Run these commands to fix:
|
||||
bun run format
|
||||
bun run lint:fix
|
||||
bun run type-check
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
The hooks are **automatically registered** when the plugin is loaded by Claude Code. No manual installation needed.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Disable Hook Temporarily
|
||||
|
||||
If you need to bypass the hook temporarily:
|
||||
|
||||
```bash
|
||||
# Set environment variable
|
||||
export SKIP_TYPESCRIPT_CHECK=1
|
||||
|
||||
# Or disable in Claude Code settings
|
||||
```
|
||||
|
||||
### Customize Hook Behavior
|
||||
|
||||
Edit `typescript-check.sh` to adjust:
|
||||
|
||||
- Which tools to run (tsc, biome)
|
||||
- Error message formatting
|
||||
- Suggested fix commands
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Hook Not Running
|
||||
|
||||
**Check:**
|
||||
|
||||
1. Plugin is properly loaded
|
||||
2. `typescript-check.sh` is executable: `chmod +x hooks/typescript-check.sh`
|
||||
3. `hooks.json` is valid JSON
|
||||
4. Hook log file: `~/.claude/hooks/typescript-check.log`
|
||||
|
||||
### False Positives
|
||||
|
||||
If the hook is too strict:
|
||||
|
||||
1. **Adjust Biome rules** in project's `biome.json`
|
||||
2. **Exclude files** in `.gitignore` or Biome config
|
||||
3. **Disable specific rules** for certain files
|
||||
|
||||
### Performance Issues
|
||||
|
||||
If hook is too slow:
|
||||
|
||||
1. **Run only on changed files** (modify script to check specific file)
|
||||
2. **Disable type checking for large projects** (comment out `tsc` in script)
|
||||
3. **Use incremental TypeScript** (add `--incremental` flag)
|
||||
|
||||
## Log File
|
||||
|
||||
Hook execution is logged to:
|
||||
|
||||
```
|
||||
~/.claude/hooks/typescript-check.log
|
||||
```
|
||||
|
||||
View logs:
|
||||
|
||||
```bash
|
||||
tail -f ~/.claude/hooks/typescript-check.log
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep hook fast**: Only run essential checks
|
||||
2. **Provide clear errors**: Show exact line numbers and fixes
|
||||
3. **Log everything**: Use the log file for debugging
|
||||
4. **Test thoroughly**: Ensure hook doesn't block valid operations
|
||||
5. **Document behavior**: Explain what the hook does and why
|
||||
|
||||
## Comparison with Pre-commit Hooks
|
||||
|
||||
| Feature | Claude Code Hook | Git Pre-commit Hook |
|
||||
| ------------ | -------------------- | ------------------------- |
|
||||
| **Trigger** | On file Write/Edit | On git commit |
|
||||
| **Scope** | Single file | Staged files |
|
||||
| **Speed** | Per-file validation | Batch validation |
|
||||
| **Blocking** | Blocks file creation | Blocks commit |
|
||||
| **Best for** | Real-time feedback | Final check before commit |
|
||||
|
||||
**Recommendation**: Use **both**:
|
||||
|
||||
- Claude Code hooks for immediate feedback during development
|
||||
- Pre-commit hooks for final validation before committing
|
||||
|
||||
## Related
|
||||
|
||||
- **Pre-commit hooks**: See `templates/.husky/` for Git pre-commit hooks
|
||||
- **Quality gates**: See `commands/quality-gates.md` for full workflow
|
||||
- **TypeScript config**: Adjust `tsconfig.json` for type checking behavior
|
||||
- **Biome config**: Adjust `biome.json` for linting behavior
|
||||
16
hooks/hooks.json
Normal file
16
hooks/hooks.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"description": "TypeScript and code quality validation hook for Write/Edit operations on .ts/.tsx files",
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/typescript-check.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
196
hooks/typescript-check.sh
Executable file
196
hooks/typescript-check.sh
Executable file
@@ -0,0 +1,196 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# TypeScript and Code Quality Check Hook
|
||||
#
|
||||
# This hook validates TypeScript files when they are created or modified.
|
||||
# It runs TypeScript type checking and Biome linting to ensure code quality.
|
||||
#
|
||||
# Installation:
|
||||
# 1. Copy to: .claude/hooks/on-tool-use/typescript-check.sh
|
||||
# 2. Make executable: chmod +x .claude/hooks/on-tool-use/typescript-check.sh
|
||||
# 3. Configure Claude Code to use this hook for Write/Edit operations
|
||||
#
|
||||
|
||||
# Set up logging
|
||||
LOG_FILE="$HOME/.claude/hooks/typescript-check.log"
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
# Function to log with timestamp
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Start logging
|
||||
log "=== Hook execution started (JSON mode) ==="
|
||||
|
||||
# Read JSON input from stdin
|
||||
input=$(cat)
|
||||
|
||||
# Log the raw input
|
||||
log "Raw input received: $input"
|
||||
|
||||
# Extract file path from the JSON input
|
||||
file_path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.target_file // empty')
|
||||
|
||||
log "Extracted file_path: '$file_path'"
|
||||
|
||||
# Check if file path is not empty and is a TypeScript file
|
||||
if [[ -n "$file_path" && "$file_path" != "null" && "$file_path" != "empty" ]]; then
|
||||
log "File path is not empty: $file_path"
|
||||
|
||||
# Check if it's a TypeScript file
|
||||
if [[ "$file_path" == *.ts || "$file_path" == *.tsx ]]; then
|
||||
log "File is a TypeScript file: $file_path"
|
||||
|
||||
# Check if the file actually exists
|
||||
if [[ -f "$file_path" ]]; then
|
||||
log "Running quality checks on $file_path..."
|
||||
|
||||
# Get the directory containing the TypeScript file
|
||||
dir=$(dirname "$file_path")
|
||||
log "File directory: $dir"
|
||||
|
||||
# Look for project root (contains package.json)
|
||||
project_root="$dir"
|
||||
while [[ "$project_root" != "/" ]]; do
|
||||
if [[ -f "$project_root/package.json" ]]; then
|
||||
log "Found project root at: $project_root"
|
||||
break
|
||||
fi
|
||||
project_root=$(dirname "$project_root")
|
||||
done
|
||||
|
||||
# If no package.json found, use file directory
|
||||
if [[ ! -f "$project_root/package.json" ]]; then
|
||||
log "No package.json found, using file directory: $dir"
|
||||
project_root="$dir"
|
||||
fi
|
||||
|
||||
# Function to find command in project
|
||||
find_cmd() {
|
||||
local cmd_name=$1
|
||||
local paths=(
|
||||
"$(command -v "$cmd_name" 2>/dev/null)"
|
||||
"$project_root/node_modules/.bin/$cmd_name"
|
||||
"$HOME/.bun/bin/$cmd_name"
|
||||
"/usr/local/bin/$cmd_name"
|
||||
"/opt/homebrew/bin/$cmd_name"
|
||||
)
|
||||
|
||||
for path in "${paths[@]}"; do
|
||||
if [[ -x "$path" && -n "$path" ]]; then
|
||||
echo "$path"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Variables to track results
|
||||
tsc_success=true
|
||||
biome_success=true
|
||||
all_output=""
|
||||
|
||||
# Change to project root for all operations
|
||||
cd "$project_root" || exit 1
|
||||
|
||||
# 1. Run TypeScript type check
|
||||
tsc_cmd=$(find_cmd tsc)
|
||||
if [[ -n "$tsc_cmd" ]]; then
|
||||
log "Found tsc at: $tsc_cmd"
|
||||
log "Running: $tsc_cmd --noEmit"
|
||||
|
||||
tsc_output=$("$tsc_cmd" --noEmit 2>&1)
|
||||
tsc_exit_code=$?
|
||||
|
||||
if [[ -n "$tsc_output" ]]; then
|
||||
log "tsc output: $tsc_output"
|
||||
all_output="TypeScript Check:\n$tsc_output\n\n"
|
||||
fi
|
||||
|
||||
log "tsc exit code: $tsc_exit_code"
|
||||
|
||||
if [[ $tsc_exit_code -ne 0 ]]; then
|
||||
log "TypeScript check found type errors"
|
||||
tsc_success=false
|
||||
fi
|
||||
else
|
||||
log "WARNING: TypeScript compiler not found"
|
||||
all_output="WARNING: TypeScript compiler not found. Install TypeScript to enable type checking.\n\n"
|
||||
fi
|
||||
|
||||
# 2. Run Biome check
|
||||
biome_cmd=$(find_cmd biome)
|
||||
if [[ -n "$biome_cmd" ]]; then
|
||||
log "Found biome at: $biome_cmd"
|
||||
log "Running: $biome_cmd check $file_path"
|
||||
|
||||
biome_output=$("$biome_cmd" check "$file_path" 2>&1)
|
||||
biome_exit_code=$?
|
||||
|
||||
if [[ -n "$biome_output" ]]; then
|
||||
log "biome output: $biome_output"
|
||||
all_output="${all_output}Biome Check:\n$biome_output"
|
||||
fi
|
||||
|
||||
log "biome exit code: $biome_exit_code"
|
||||
|
||||
if [[ $biome_exit_code -ne 0 ]]; then
|
||||
log "Biome check found errors"
|
||||
biome_success=false
|
||||
fi
|
||||
else
|
||||
log "WARNING: Biome not found"
|
||||
all_output="${all_output}WARNING: Biome not found. Install @biomejs/biome to enable linting."
|
||||
fi
|
||||
|
||||
# Determine final result
|
||||
if [[ "$tsc_success" == true && "$biome_success" == true ]]; then
|
||||
log "All checks passed successfully"
|
||||
decision='{"suppressOutput": false}'
|
||||
log "Hook decision: $decision"
|
||||
echo "$decision"
|
||||
else
|
||||
# Errors found - block the operation
|
||||
log "Checks failed - blocking operation"
|
||||
|
||||
reason="Code quality checks failed for $file_path"
|
||||
|
||||
if [[ "$tsc_success" == false ]]; then
|
||||
reason="$reason\n\n❌ TypeScript errors found"
|
||||
fi
|
||||
|
||||
if [[ "$biome_success" == false ]]; then
|
||||
reason="$reason\n\n❌ Biome lint/format errors found"
|
||||
fi
|
||||
|
||||
if [[ -n "$all_output" ]]; then
|
||||
reason="$reason:\n\n$all_output\n\n💡 Run these commands to fix:\n bun run format\n bun run lint:fix\n bun run type-check"
|
||||
else
|
||||
reason="$reason. Please fix these issues before proceeding."
|
||||
fi
|
||||
|
||||
# Return blocking JSON response
|
||||
decision=$(jq -n --arg reason "$reason" '{
|
||||
"decision": "block",
|
||||
"reason": $reason
|
||||
}')
|
||||
log "Hook decision: $decision"
|
||||
echo "$decision"
|
||||
fi
|
||||
else
|
||||
log "File does not exist: $file_path"
|
||||
echo '{}'
|
||||
fi
|
||||
else
|
||||
log "Not a TypeScript file, skipping: $file_path"
|
||||
echo '{}'
|
||||
fi
|
||||
else
|
||||
log "File path is empty or null, skipping"
|
||||
echo '{}'
|
||||
fi
|
||||
|
||||
log "=== Hook execution completed (JSON mode) ==="
|
||||
log ""
|
||||
Reference in New Issue
Block a user