mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
docs: add hooks guide, expand planner agent, add Django example
- Add hooks/README.md: comprehensive hook documentation with input schema, customization guide, 4 ready-to-use hook recipes, and cross-platform notes - Expand planner agent with full worked example (Stripe subscriptions plan) and sizing/phasing guidance (119 → 212 lines) - Add Django REST API example config (DRF + Celery + pytest + Factory Boy) - Update README directory tree with new files
This commit is contained in:
@@ -289,6 +289,7 @@ everything-claude-code/
|
|||||||
| |-- golang/ # Go specific
|
| |-- golang/ # Go specific
|
||||||
|
|
|
|
||||||
|-- hooks/ # Trigger-based automations
|
|-- hooks/ # Trigger-based automations
|
||||||
|
| |-- README.md # Hook documentation, recipes, and customization guide
|
||||||
| |-- hooks.json # All hooks config (PreToolUse, PostToolUse, Stop, etc.)
|
| |-- hooks.json # All hooks config (PreToolUse, PostToolUse, Stop, etc.)
|
||||||
| |-- memory-persistence/ # Session lifecycle hooks (Longform Guide)
|
| |-- memory-persistence/ # Session lifecycle hooks (Longform Guide)
|
||||||
| |-- strategic-compact/ # Compaction suggestions (Longform Guide)
|
| |-- strategic-compact/ # Compaction suggestions (Longform Guide)
|
||||||
@@ -320,6 +321,7 @@ everything-claude-code/
|
|||||||
| |-- user-CLAUDE.md # Example user-level config
|
| |-- user-CLAUDE.md # Example user-level config
|
||||||
| |-- saas-nextjs-CLAUDE.md # Real-world SaaS (Next.js + Supabase + Stripe)
|
| |-- saas-nextjs-CLAUDE.md # Real-world SaaS (Next.js + Supabase + Stripe)
|
||||||
| |-- go-microservice-CLAUDE.md # Real-world Go microservice (gRPC + PostgreSQL)
|
| |-- go-microservice-CLAUDE.md # Real-world Go microservice (gRPC + PostgreSQL)
|
||||||
|
| |-- django-api-CLAUDE.md # Real-world Django REST API (DRF + Celery)
|
||||||
|
|
|
|
||||||
|-- mcp-configs/ # MCP server configurations
|
|-- mcp-configs/ # MCP server configurations
|
||||||
| |-- mcp-servers.json # GitHub, Supabase, Vercel, Railway, etc.
|
| |-- mcp-servers.json # GitHub, Supabase, Vercel, Railway, etc.
|
||||||
|
|||||||
@@ -98,6 +98,85 @@ Create detailed steps with:
|
|||||||
6. **Think Incrementally**: Each step should be verifiable
|
6. **Think Incrementally**: Each step should be verifiable
|
||||||
7. **Document Decisions**: Explain why, not just what
|
7. **Document Decisions**: Explain why, not just what
|
||||||
|
|
||||||
|
## Worked Example: Adding Stripe Subscriptions
|
||||||
|
|
||||||
|
Here is a complete plan showing the level of detail expected:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Implementation Plan: Stripe Subscription Billing
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Add subscription billing with free/pro/enterprise tiers. Users upgrade via
|
||||||
|
Stripe Checkout, and webhook events keep subscription status in sync.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
- Three tiers: Free (default), Pro ($29/mo), Enterprise ($99/mo)
|
||||||
|
- Stripe Checkout for payment flow
|
||||||
|
- Webhook handler for subscription lifecycle events
|
||||||
|
- Feature gating based on subscription tier
|
||||||
|
|
||||||
|
## Architecture Changes
|
||||||
|
- New table: `subscriptions` (user_id, stripe_customer_id, stripe_subscription_id, status, tier)
|
||||||
|
- New API route: `app/api/checkout/route.ts` — creates Stripe Checkout session
|
||||||
|
- New API route: `app/api/webhooks/stripe/route.ts` — handles Stripe events
|
||||||
|
- New middleware: check subscription tier for gated features
|
||||||
|
- New component: `PricingTable` — displays tiers with upgrade buttons
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
### Phase 1: Database & Backend (2 files)
|
||||||
|
1. **Create subscription migration** (File: supabase/migrations/004_subscriptions.sql)
|
||||||
|
- Action: CREATE TABLE subscriptions with RLS policies
|
||||||
|
- Why: Store billing state server-side, never trust client
|
||||||
|
- Dependencies: None
|
||||||
|
- Risk: Low
|
||||||
|
|
||||||
|
2. **Create Stripe webhook handler** (File: src/app/api/webhooks/stripe/route.ts)
|
||||||
|
- Action: Handle checkout.session.completed, customer.subscription.updated,
|
||||||
|
customer.subscription.deleted events
|
||||||
|
- Why: Keep subscription status in sync with Stripe
|
||||||
|
- Dependencies: Step 1 (needs subscriptions table)
|
||||||
|
- Risk: High — webhook signature verification is critical
|
||||||
|
|
||||||
|
### Phase 2: Checkout Flow (2 files)
|
||||||
|
3. **Create checkout API route** (File: src/app/api/checkout/route.ts)
|
||||||
|
- Action: Create Stripe Checkout session with price_id and success/cancel URLs
|
||||||
|
- Why: Server-side session creation prevents price tampering
|
||||||
|
- Dependencies: Step 1
|
||||||
|
- Risk: Medium — must validate user is authenticated
|
||||||
|
|
||||||
|
4. **Build pricing page** (File: src/components/PricingTable.tsx)
|
||||||
|
- Action: Display three tiers with feature comparison and upgrade buttons
|
||||||
|
- Why: User-facing upgrade flow
|
||||||
|
- Dependencies: Step 3
|
||||||
|
- Risk: Low
|
||||||
|
|
||||||
|
### Phase 3: Feature Gating (1 file)
|
||||||
|
5. **Add tier-based middleware** (File: src/middleware.ts)
|
||||||
|
- Action: Check subscription tier on protected routes, redirect free users
|
||||||
|
- Why: Enforce tier limits server-side
|
||||||
|
- Dependencies: Steps 1-2 (needs subscription data)
|
||||||
|
- Risk: Medium — must handle edge cases (expired, past_due)
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
- Unit tests: Webhook event parsing, tier checking logic
|
||||||
|
- Integration tests: Checkout session creation, webhook processing
|
||||||
|
- E2E tests: Full upgrade flow (Stripe test mode)
|
||||||
|
|
||||||
|
## Risks & Mitigations
|
||||||
|
- **Risk**: Webhook events arrive out of order
|
||||||
|
- Mitigation: Use event timestamps, idempotent updates
|
||||||
|
- **Risk**: User upgrades but webhook fails
|
||||||
|
- Mitigation: Poll Stripe as fallback, show "processing" state
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
- [ ] User can upgrade from Free to Pro via Stripe Checkout
|
||||||
|
- [ ] Webhook correctly syncs subscription status
|
||||||
|
- [ ] Free users cannot access Pro features
|
||||||
|
- [ ] Downgrade/cancellation works correctly
|
||||||
|
- [ ] All tests pass with 80%+ coverage
|
||||||
|
```
|
||||||
|
|
||||||
## When Planning Refactors
|
## When Planning Refactors
|
||||||
|
|
||||||
1. Identify code smells and technical debt
|
1. Identify code smells and technical debt
|
||||||
@@ -106,6 +185,17 @@ Create detailed steps with:
|
|||||||
4. Create backwards-compatible changes when possible
|
4. Create backwards-compatible changes when possible
|
||||||
5. Plan for gradual migration if needed
|
5. Plan for gradual migration if needed
|
||||||
|
|
||||||
|
## Sizing and Phasing
|
||||||
|
|
||||||
|
When the feature is large, break it into independently deliverable phases:
|
||||||
|
|
||||||
|
- **Phase 1**: Minimum viable — smallest slice that provides value
|
||||||
|
- **Phase 2**: Core experience — complete happy path
|
||||||
|
- **Phase 3**: Edge cases — error handling, edge cases, polish
|
||||||
|
- **Phase 4**: Optimization — performance, monitoring, analytics
|
||||||
|
|
||||||
|
Each phase should be mergeable independently. Avoid plans that require all phases to complete before anything works.
|
||||||
|
|
||||||
## Red Flags to Check
|
## Red Flags to Check
|
||||||
|
|
||||||
- Large functions (>50 lines)
|
- Large functions (>50 lines)
|
||||||
@@ -115,5 +205,8 @@ Create detailed steps with:
|
|||||||
- Hardcoded values
|
- Hardcoded values
|
||||||
- Missing tests
|
- Missing tests
|
||||||
- Performance bottlenecks
|
- Performance bottlenecks
|
||||||
|
- Plans with no testing strategy
|
||||||
|
- Steps without clear file paths
|
||||||
|
- Phases that cannot be delivered independently
|
||||||
|
|
||||||
**Remember**: A great plan is specific, actionable, and considers both the happy path and edge cases. The best plans enable confident, incremental implementation.
|
**Remember**: A great plan is specific, actionable, and considers both the happy path and edge cases. The best plans enable confident, incremental implementation.
|
||||||
|
|||||||
308
examples/django-api-CLAUDE.md
Normal file
308
examples/django-api-CLAUDE.md
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
# Django REST API — Project CLAUDE.md
|
||||||
|
|
||||||
|
> Real-world example for a Django REST Framework API with PostgreSQL and Celery.
|
||||||
|
> Copy this to your project root and customize for your service.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**Stack:** Python 3.12+, Django 5.x, Django REST Framework, PostgreSQL, Celery + Redis, pytest, Docker Compose
|
||||||
|
|
||||||
|
**Architecture:** Domain-driven design with apps per business domain. DRF for API layer, Celery for async tasks, pytest for testing. All endpoints return JSON — no template rendering.
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
### Python Conventions
|
||||||
|
|
||||||
|
- Type hints on all function signatures — use `from __future__ import annotations`
|
||||||
|
- No `print()` statements — use `logging.getLogger(__name__)`
|
||||||
|
- f-strings for string formatting, never `%` or `.format()`
|
||||||
|
- Use `pathlib.Path` not `os.path` for file operations
|
||||||
|
- Imports sorted with isort: stdlib, third-party, local (enforced by ruff)
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
- All queries use Django ORM — raw SQL only with `.raw()` and parameterized queries
|
||||||
|
- Migrations committed to git — never use `--fake` in production
|
||||||
|
- Use `select_related()` and `prefetch_related()` to prevent N+1 queries
|
||||||
|
- All models must have `created_at` and `updated_at` auto-fields
|
||||||
|
- Indexes on any field used in `filter()`, `order_by()`, or `WHERE` clauses
|
||||||
|
|
||||||
|
```python
|
||||||
|
# BAD: N+1 query
|
||||||
|
orders = Order.objects.all()
|
||||||
|
for order in orders:
|
||||||
|
print(order.customer.name) # hits DB for each order
|
||||||
|
|
||||||
|
# GOOD: Single query with join
|
||||||
|
orders = Order.objects.select_related("customer").all()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
- JWT via `djangorestframework-simplejwt` — access token (15 min) + refresh token (7 days)
|
||||||
|
- Permission classes on every view — never rely on default
|
||||||
|
- Use `IsAuthenticated` as base, add custom permissions for object-level access
|
||||||
|
- Token blacklisting enabled for logout
|
||||||
|
|
||||||
|
### Serializers
|
||||||
|
|
||||||
|
- Use `ModelSerializer` for simple CRUD, `Serializer` for complex validation
|
||||||
|
- Separate read and write serializers when input/output shapes differ
|
||||||
|
- Validate at serializer level, not in views — views should be thin
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CreateOrderSerializer(serializers.Serializer):
|
||||||
|
product_id = serializers.UUIDField()
|
||||||
|
quantity = serializers.IntegerField(min_value=1, max_value=100)
|
||||||
|
|
||||||
|
def validate_product_id(self, value):
|
||||||
|
if not Product.objects.filter(id=value, active=True).exists():
|
||||||
|
raise serializers.ValidationError("Product not found or inactive")
|
||||||
|
return value
|
||||||
|
|
||||||
|
class OrderDetailSerializer(serializers.ModelSerializer):
|
||||||
|
customer = CustomerSerializer(read_only=True)
|
||||||
|
product = ProductSerializer(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Order
|
||||||
|
fields = ["id", "customer", "product", "quantity", "total", "status", "created_at"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- Use DRF exception handler for consistent error responses
|
||||||
|
- Custom exceptions for business logic in `core/exceptions.py`
|
||||||
|
- Never expose internal error details to clients
|
||||||
|
|
||||||
|
```python
|
||||||
|
# core/exceptions.py
|
||||||
|
from rest_framework.exceptions import APIException
|
||||||
|
|
||||||
|
class InsufficientStockError(APIException):
|
||||||
|
status_code = 409
|
||||||
|
default_detail = "Insufficient stock for this order"
|
||||||
|
default_code = "insufficient_stock"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
|
||||||
|
- No emojis in code or comments
|
||||||
|
- Max line length: 120 characters (enforced by ruff)
|
||||||
|
- Classes: PascalCase, functions/variables: snake_case, constants: UPPER_SNAKE_CASE
|
||||||
|
- Views are thin — business logic lives in service functions or model methods
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
settings/
|
||||||
|
base.py # Shared settings
|
||||||
|
local.py # Dev overrides (DEBUG=True)
|
||||||
|
production.py # Production settings
|
||||||
|
urls.py # Root URL config
|
||||||
|
celery.py # Celery app configuration
|
||||||
|
apps/
|
||||||
|
accounts/ # User auth, registration, profile
|
||||||
|
models.py
|
||||||
|
serializers.py
|
||||||
|
views.py
|
||||||
|
services.py # Business logic
|
||||||
|
tests/
|
||||||
|
test_views.py
|
||||||
|
test_services.py
|
||||||
|
factories.py # Factory Boy factories
|
||||||
|
orders/ # Order management
|
||||||
|
models.py
|
||||||
|
serializers.py
|
||||||
|
views.py
|
||||||
|
services.py
|
||||||
|
tasks.py # Celery tasks
|
||||||
|
tests/
|
||||||
|
products/ # Product catalog
|
||||||
|
models.py
|
||||||
|
serializers.py
|
||||||
|
views.py
|
||||||
|
tests/
|
||||||
|
core/
|
||||||
|
exceptions.py # Custom API exceptions
|
||||||
|
permissions.py # Shared permission classes
|
||||||
|
pagination.py # Custom pagination
|
||||||
|
middleware.py # Request logging, timing
|
||||||
|
tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Patterns
|
||||||
|
|
||||||
|
### Service Layer
|
||||||
|
|
||||||
|
```python
|
||||||
|
# apps/orders/services.py
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
def create_order(*, customer, product_id: uuid.UUID, quantity: int) -> Order:
|
||||||
|
"""Create an order with stock validation and payment hold."""
|
||||||
|
product = Product.objects.select_for_update().get(id=product_id)
|
||||||
|
|
||||||
|
if product.stock < quantity:
|
||||||
|
raise InsufficientStockError()
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
order = Order.objects.create(
|
||||||
|
customer=customer,
|
||||||
|
product=product,
|
||||||
|
quantity=quantity,
|
||||||
|
total=product.price * quantity,
|
||||||
|
)
|
||||||
|
product.stock -= quantity
|
||||||
|
product.save(update_fields=["stock", "updated_at"])
|
||||||
|
|
||||||
|
# Async: send confirmation email
|
||||||
|
send_order_confirmation.delay(order.id)
|
||||||
|
return order
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
# apps/orders/views.py
|
||||||
|
class OrderViewSet(viewsets.ModelViewSet):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
pagination_class = StandardPagination
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.action == "create":
|
||||||
|
return CreateOrderSerializer
|
||||||
|
return OrderDetailSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
Order.objects
|
||||||
|
.filter(customer=self.request.user)
|
||||||
|
.select_related("product", "customer")
|
||||||
|
.order_by("-created_at")
|
||||||
|
)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
order = create_order(
|
||||||
|
customer=self.request.user,
|
||||||
|
product_id=serializer.validated_data["product_id"],
|
||||||
|
quantity=serializer.validated_data["quantity"],
|
||||||
|
)
|
||||||
|
serializer.instance = order
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Pattern (pytest + Factory Boy)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# apps/orders/tests/factories.py
|
||||||
|
import factory
|
||||||
|
from apps.accounts.tests.factories import UserFactory
|
||||||
|
from apps.products.tests.factories import ProductFactory
|
||||||
|
|
||||||
|
class OrderFactory(factory.django.DjangoModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = "orders.Order"
|
||||||
|
|
||||||
|
customer = factory.SubFactory(UserFactory)
|
||||||
|
product = factory.SubFactory(ProductFactory, stock=100)
|
||||||
|
quantity = 1
|
||||||
|
total = factory.LazyAttribute(lambda o: o.product.price * o.quantity)
|
||||||
|
|
||||||
|
# apps/orders/tests/test_views.py
|
||||||
|
import pytest
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestCreateOrder:
|
||||||
|
def setup_method(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.user = UserFactory()
|
||||||
|
self.client.force_authenticate(self.user)
|
||||||
|
|
||||||
|
def test_create_order_success(self):
|
||||||
|
product = ProductFactory(price=29_99, stock=10)
|
||||||
|
response = self.client.post("/api/orders/", {
|
||||||
|
"product_id": str(product.id),
|
||||||
|
"quantity": 2,
|
||||||
|
})
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert response.data["total"] == 59_98
|
||||||
|
|
||||||
|
def test_create_order_insufficient_stock(self):
|
||||||
|
product = ProductFactory(stock=0)
|
||||||
|
response = self.client.post("/api/orders/", {
|
||||||
|
"product_id": str(product.id),
|
||||||
|
"quantity": 1,
|
||||||
|
})
|
||||||
|
assert response.status_code == 409
|
||||||
|
|
||||||
|
def test_create_order_unauthenticated(self):
|
||||||
|
self.client.force_authenticate(None)
|
||||||
|
response = self.client.post("/api/orders/", {})
|
||||||
|
assert response.status_code == 401
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Django
|
||||||
|
SECRET_KEY=
|
||||||
|
DEBUG=False
|
||||||
|
ALLOWED_HOSTS=api.example.com
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_URL=postgres://user:pass@localhost:5432/myapp
|
||||||
|
|
||||||
|
# Redis (Celery broker + cache)
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_ACCESS_TOKEN_LIFETIME=15 # minutes
|
||||||
|
JWT_REFRESH_TOKEN_LIFETIME=10080 # minutes (7 days)
|
||||||
|
|
||||||
|
# Email
|
||||||
|
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
|
||||||
|
EMAIL_HOST=smtp.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
pytest --cov=apps --cov-report=term-missing
|
||||||
|
|
||||||
|
# Run specific app tests
|
||||||
|
pytest apps/orders/tests/ -v
|
||||||
|
|
||||||
|
# Run with parallel execution
|
||||||
|
pytest -n auto
|
||||||
|
|
||||||
|
# Only failing tests from last run
|
||||||
|
pytest --lf
|
||||||
|
```
|
||||||
|
|
||||||
|
## ECC Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Planning
|
||||||
|
/plan "Add order refund system with Stripe integration"
|
||||||
|
|
||||||
|
# Development with TDD
|
||||||
|
/tdd # pytest-based TDD workflow
|
||||||
|
|
||||||
|
# Review
|
||||||
|
/python-review # Python-specific code review
|
||||||
|
/security-scan # Django security audit
|
||||||
|
/code-review # General quality check
|
||||||
|
|
||||||
|
# Verification
|
||||||
|
/verify # Build, lint, test, security scan
|
||||||
|
```
|
||||||
|
|
||||||
|
## Git Workflow
|
||||||
|
|
||||||
|
- `feat:` new features, `fix:` bug fixes, `refactor:` code changes
|
||||||
|
- Feature branches from `main`, PRs required
|
||||||
|
- CI: ruff (lint + format), mypy (types), pytest (tests), safety (dep check)
|
||||||
|
- Deploy: Docker image, managed via Kubernetes or Railway
|
||||||
198
hooks/README.md
Normal file
198
hooks/README.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# Hooks
|
||||||
|
|
||||||
|
Hooks are event-driven automations that fire before or after Claude Code tool executions. They enforce code quality, catch mistakes early, and automate repetitive checks.
|
||||||
|
|
||||||
|
## How Hooks Work
|
||||||
|
|
||||||
|
```
|
||||||
|
User request → Claude picks a tool → PreToolUse hook runs → Tool executes → PostToolUse hook runs
|
||||||
|
```
|
||||||
|
|
||||||
|
- **PreToolUse** hooks run before the tool executes. They can **block** (exit code 2) or **warn** (stderr without blocking).
|
||||||
|
- **PostToolUse** hooks run after the tool completes. They can analyze output but cannot block.
|
||||||
|
- **Stop** hooks run after each Claude response.
|
||||||
|
- **SessionStart/SessionEnd** hooks run at session lifecycle boundaries.
|
||||||
|
- **PreCompact** hooks run before context compaction, useful for saving state.
|
||||||
|
|
||||||
|
## Hooks in This Plugin
|
||||||
|
|
||||||
|
### PreToolUse Hooks
|
||||||
|
|
||||||
|
| Hook | Matcher | Behavior | Exit Code |
|
||||||
|
|------|---------|----------|-----------|
|
||||||
|
| **Dev server blocker** | `Bash` | Blocks `npm run dev` etc. outside tmux — ensures log access | 2 (blocks) |
|
||||||
|
| **Tmux reminder** | `Bash` | Suggests tmux for long-running commands (npm test, cargo build, docker) | 0 (warns) |
|
||||||
|
| **Git push reminder** | `Bash` | Reminds to review changes before `git push` | 0 (warns) |
|
||||||
|
| **Doc file blocker** | `Write` | Blocks creation of random `.md`/`.txt` files (allows README, CLAUDE, CONTRIBUTING) | 2 (blocks) |
|
||||||
|
| **Strategic compact** | `Edit\|Write` | Suggests manual `/compact` at logical intervals (every ~50 tool calls) | 0 (warns) |
|
||||||
|
|
||||||
|
### PostToolUse Hooks
|
||||||
|
|
||||||
|
| Hook | Matcher | What It Does |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| **PR logger** | `Bash` | Logs PR URL and review command after `gh pr create` |
|
||||||
|
| **Build analysis** | `Bash` | Background analysis after build commands (async, non-blocking) |
|
||||||
|
| **Prettier format** | `Edit` | Auto-formats JS/TS files with Prettier after edits |
|
||||||
|
| **TypeScript check** | `Edit` | Runs `tsc --noEmit` after editing `.ts`/`.tsx` files |
|
||||||
|
| **console.log warning** | `Edit` | Warns about `console.log` statements in edited files |
|
||||||
|
|
||||||
|
### Lifecycle Hooks
|
||||||
|
|
||||||
|
| Hook | Event | What It Does |
|
||||||
|
|------|-------|-------------|
|
||||||
|
| **Session start** | `SessionStart` | Loads previous context and detects package manager |
|
||||||
|
| **Pre-compact** | `PreCompact` | Saves state before context compaction |
|
||||||
|
| **Console.log audit** | `Stop` | Checks all modified files for `console.log` after each response |
|
||||||
|
| **Session end** | `SessionEnd` | Persists session state for next session |
|
||||||
|
| **Pattern extraction** | `SessionEnd` | Evaluates session for extractable patterns (continuous learning) |
|
||||||
|
|
||||||
|
## Customizing Hooks
|
||||||
|
|
||||||
|
### Disabling a Hook
|
||||||
|
|
||||||
|
Remove or comment out the hook entry in `hooks.json`. If installed as a plugin, override in your `~/.claude/settings.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Write",
|
||||||
|
"hooks": [],
|
||||||
|
"description": "Override: allow all .md file creation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writing Your Own Hook
|
||||||
|
|
||||||
|
Hooks are shell commands that receive tool input as JSON on stdin and must output JSON on stdout.
|
||||||
|
|
||||||
|
**Basic structure:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// my-hook.js
|
||||||
|
let data = '';
|
||||||
|
process.stdin.on('data', chunk => data += chunk);
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
const input = JSON.parse(data);
|
||||||
|
|
||||||
|
// Access tool info
|
||||||
|
const toolName = input.tool_name; // "Edit", "Bash", "Write", etc.
|
||||||
|
const toolInput = input.tool_input; // Tool-specific parameters
|
||||||
|
const toolOutput = input.tool_output; // Only available in PostToolUse
|
||||||
|
|
||||||
|
// Warn (non-blocking): write to stderr
|
||||||
|
console.error('[Hook] Warning message shown to Claude');
|
||||||
|
|
||||||
|
// Block (PreToolUse only): exit with code 2
|
||||||
|
// process.exit(2);
|
||||||
|
|
||||||
|
// Always output the original data to stdout
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exit codes:**
|
||||||
|
- `0` — Success (continue execution)
|
||||||
|
- `2` — Block the tool call (PreToolUse only)
|
||||||
|
- Other non-zero — Error (logged but does not block)
|
||||||
|
|
||||||
|
### Hook Input Schema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface HookInput {
|
||||||
|
tool_name: string; // "Bash", "Edit", "Write", "Read", etc.
|
||||||
|
tool_input: {
|
||||||
|
command?: string; // Bash: the command being run
|
||||||
|
file_path?: string; // Edit/Write/Read: target file
|
||||||
|
old_string?: string; // Edit: text being replaced
|
||||||
|
new_string?: string; // Edit: replacement text
|
||||||
|
content?: string; // Write: file content
|
||||||
|
};
|
||||||
|
tool_output?: { // PostToolUse only
|
||||||
|
output?: string; // Command/tool output
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Async Hooks
|
||||||
|
|
||||||
|
For hooks that should not block the main flow (e.g., background analysis):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node my-slow-hook.js",
|
||||||
|
"async": true,
|
||||||
|
"timeout": 30
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Async hooks run in the background. They cannot block tool execution.
|
||||||
|
|
||||||
|
## Common Hook Recipes
|
||||||
|
|
||||||
|
### Warn about TODO comments
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"matcher": "Edit",
|
||||||
|
"hooks": [{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const ns=i.tool_input?.new_string||'';if(/TODO|FIXME|HACK/.test(ns)){console.error('[Hook] New TODO/FIXME added - consider creating an issue')}console.log(d)})\""
|
||||||
|
}],
|
||||||
|
"description": "Warn when adding TODO/FIXME comments"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Block large file creation
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"matcher": "Write",
|
||||||
|
"hooks": [{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const c=i.tool_input?.content||'';const lines=c.split('\\n').length;if(lines>800){console.error('[Hook] BLOCKED: File exceeds 800 lines ('+lines+' lines)');console.error('[Hook] Split into smaller, focused modules');process.exit(2)}console.log(d)})\""
|
||||||
|
}],
|
||||||
|
"description": "Block creation of files larger than 800 lines"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-format Python files with ruff
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"matcher": "Edit",
|
||||||
|
"hooks": [{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';if(/\\.py$/.test(p)){const{execFileSync}=require('child_process');try{execFileSync('ruff',['format',p],{stdio:'pipe'})}catch(e){}}console.log(d)})\""
|
||||||
|
}],
|
||||||
|
"description": "Auto-format Python files with ruff after edits"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Require test files alongside new source files
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"matcher": "Write",
|
||||||
|
"hooks": [{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node -e \"const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';if(/src\\/.*\\.(ts|js)$/.test(p)&&!/\\.test\\.|\\.spec\\./.test(p)){const testPath=p.replace(/\\.(ts|js)$/,'.test.$1');if(!fs.existsSync(testPath)){console.error('[Hook] No test file found for: '+p);console.error('[Hook] Expected: '+testPath);console.error('[Hook] Consider writing tests first (/tdd)')}}console.log(d)})\""
|
||||||
|
}],
|
||||||
|
"description": "Remind to create tests when adding new source files"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cross-Platform Notes
|
||||||
|
|
||||||
|
All hooks in this plugin use Node.js (`node -e` or `node script.js`) for maximum compatibility across Windows, macOS, and Linux. Avoid bash-specific syntax in hooks.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [rules/common/hooks.md](../rules/common/hooks.md) — Hook architecture guidelines
|
||||||
|
- [skills/strategic-compact/](../skills/strategic-compact/) — Strategic compaction skill
|
||||||
|
- [scripts/hooks/](../scripts/hooks/) — Hook script implementations
|
||||||
Reference in New Issue
Block a user