| name | write-hook |
| description | Create Claude Code hooks for static analysis and code quality. Provides pre-built linting, formatting, and type-checking hooks. Use when setting up project quality gates, enforcing code standards, or adding pre-commit style checks. |
Hook Creator
Create hooks that enforce code quality through static analysis.
Core Principles
- Exit code 2 blocks - Use to prevent bad code from being written
- Fast checks only - Hooks run synchronously; keep under 5 seconds
- JSON stdin/stdout - Hooks receive context, can output structured responses
- Fail safe - Non-zero (except 2) continues execution with warning
- Use Python - Wrap shell commands in Python for cross-platform compatibility
Quick Start
Add a static check hook:
python3 scripts/add_hook.py <hook-name> --type <lint|format|typecheck|custom>
Options:
--type,-t: Hook template type (default:custom)--path,-p: Output directory (default:.claude/hooks)--event,-e: Hook event (default:PostToolUse)
Hook Structure
.claude/
├── hooks/
│ ├── eslint_check.py # Linting hook
│ ├── prettier_check.py # Formatting hook
│ └── typecheck.py # Type checking hook
└── settings.json # Hook configuration
Configuration Format
Important: Hooks must be configured in .claude/settings.json (not settings.local.json). The settings.json file is shared with the team via version control, ensuring consistent hook behavior across all collaborators.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/eslint_check.py",
"timeout": 30
}]
}
]
}
}
Static Check Patterns
1. Linting (ESLint, Ruff, etc.)
#!/usr/bin/env python3
import json, subprocess, sys
data = json.load(sys.stdin)
file_path = data.get('tool_input', {}).get('file_path', '')
if not file_path.endswith(('.js', '.ts', '.tsx')):
sys.exit(0)
result = subprocess.run(
['npx', 'eslint', '--format', 'compact', file_path],
capture_output=True, text=True
)
if result.returncode != 0:
print(f"ESLint errors:\n{result.stdout}", file=sys.stderr)
sys.exit(2) # Block the action
2. Formatting Check
#!/usr/bin/env python3
import json, subprocess, sys
data = json.load(sys.stdin)
file_path = data.get('tool_input', {}).get('file_path', '')
result = subprocess.run(
['npx', 'prettier', '--check', file_path],
capture_output=True, text=True
)
if result.returncode != 0:
# Auto-fix instead of blocking
subprocess.run(['npx', 'prettier', '--write', file_path])
print(f"Auto-formatted: {file_path}")
3. Type Checking
#!/usr/bin/env python3
import json, subprocess, sys
data = json.load(sys.stdin)
file_path = data.get('tool_input', {}).get('file_path', '')
if not file_path.endswith(('.ts', '.tsx')):
sys.exit(0)
result = subprocess.run(
['npx', 'tsc', '--noEmit', '--pretty', 'false'],
capture_output=True, text=True
)
if result.returncode != 0:
# Filter errors for this file only
errors = [l for l in result.stdout.split('\n') if file_path in l]
if errors:
print('\n'.join(errors[:5]), file=sys.stderr)
sys.exit(2)
Event Selection Guide
| Event | Use Case | Blocking |
|---|---|---|
PreToolUse |
Validate before write | Yes |
PostToolUse |
Check after write | Yes |
UserPromptSubmit |
Add linting context | Yes |
Exit Codes
| Code | Meaning | Effect |
|---|---|---|
| 0 | Success | Continue, stdout shown in verbose |
| 2 | Block | Action blocked, stderr fed to Claude |
| Other | Warning | Continue, stderr shown in verbose |
Verification
After creating a hook, verify it works:
# Test with sample input
echo '{"tool_input":{"file_path":"test.ts"}}' | .claude/hooks/your_hook.py
echo $? # Should be 0 (pass) or 2 (block)
Check:
- Exit code 0 for valid files, 2 for violations
- Errors go to stderr, info to stdout
Best Practices
- Filter by extension - Only run checks on relevant files
- Cache dependencies - Avoid npm/pip on every check
- Limit output - Show first 5-10 errors, not all
- Auto-fix when safe - Format instead of block
References
- static-checks.md - Complete hook examples
- events.md - Hook events reference