Module 7: Remote Control and CLAUDE.md for Teams
4. CLAUDE.md for Teams — Structure, Merge, and Enforcement
4. CLAUDE.md for Teams — Structure, Merge, and Enforcement
Description
Until now, CLAUDE.md has been your personal document. You write it for your project, with your preferences, and Claude Code follows it. It works well when you're the only developer. But when three people work on the same repo, each with their own CLAUDE.md (or none), the result is chaos: files with tabs and spaces mixed, functions with and without type hints, tests in different frameworks, imports organized three different ways. The code looks like three different people wrote it — because three agents with different rules did.
CLAUDE.md for teams solves this. It's a shared document that gets committed to the repository and establishes the rules that all agents respect: code conventions, architectural decisions, forbidden patterns, testing requirements. When someone clones the repo, their Claude Code automatically adopts the team's rules. There's no "style alignment" meeting. There's no PR with 47 comments about formatting.
This capsule covers the structure of a production-ready team CLAUDE.md, the merge hierarchy that allows shared rules with personal overrides, enforcement with hooks that validate compliance, and a complete template you can adapt to your team.
CLAUDE.md as a Constitution
The right metaphor
Don't think of CLAUDE.md as a configuration file. A config file says "use port 3000." A constitution says "these are our values, these are the rules, these are the consequences of breaking them." A team CLAUDE.md is a constitution:
Config file: Constitution (CLAUDE.md):
───────────── ─────────────────────────
port: 3000 We use TypeScript strict
debug: true We don't use any - ever
log_level: info Unit tests for all business logic
Minimum coverage: 80%
Conventional commits required
No console.log in production
Imports ordered: stdlib > third-party > local
The difference: a config file describes technical parameters. A constitution describes behavioral expectations. CLAUDE.md says how the agent should behave, what patterns to follow, what to never do, and what quality level is expected.
Why it works as a standard
- It gets committed to the repo → Everyone has it automatically
- It's declarative → "what to do", not "how to configure"
- It's evolutionary → It updates via PRs like any other file
- It's enforceable → Hooks can validate compliance
- It scales without meetings → New member clones, adopts, produces consistently
Structure of a Team CLAUDE.md
Recommended sections
An effective team CLAUDE.md has these sections:
# CLAUDE.md — [Project Name]
## Project Context
Brief description of what the project does, tech stack,
and high-level architecture.
## Code Conventions
Style, naming, and formatting rules that all code must follow.
## Architecture
Current architectural decisions. Patterns we use and why.
## Forbidden Patterns
What must NEVER be done. Project-specific anti-patterns.
## Testing
Testing requirements. Frameworks, coverage, required test types.
## Git and Workflow
Commit, branch, and PR conventions.
## Security
Security rules. What to never expose, how to handle secrets.
## Dependencies
Dependency policies. When to add, how to evaluate, what to avoid.
Production-ready template
This is a CLAUDE.md you can adapt for your team:
# CLAUDE.md — TaskFlow API
## Project Context
TaskFlow is a REST API for collaborative task management.
- Backend: Python 3.12 + FastAPI
- Database: PostgreSQL 16 + SQLAlchemy 2.0 (async)
- Cache: Redis 7
- Auth: JWT with refresh tokens
- Tests: pytest + httpx
- Deploy: Docker + GitHub Actions
## Code Conventions
### Python
- Type hints required on ALL functions (parameters and return)
- Docstrings in Google format for public functions
- Descriptive variable names, no abbreviations (user_repository, NOT ur)
- Functions of at most 30 lines. If longer, extract subfunctions
- Use pathlib instead of os.path
- f-strings for interpolation, not .format() or %
### Naming
- Files: snake_case (user_service.py)
- Classes: PascalCase (UserService)
- Functions and variables: snake_case (get_user_by_id)
- Constants: UPPER_SNAKE_CASE (MAX_RETRY_COUNT)
- Endpoints: kebab-case in URLs (/api/v1/user-tasks)
### Imports
Strict order, separated by blank lines:
1. stdlib (os, sys, typing, datetime)
2. third-party (fastapi, sqlalchemy, pydantic)
3. local (app.models, app.services)
### Formatting
- ruff as linter and formatter
- Configuration in pyproject.toml (already included in the repo)
- Run `ruff check --fix .` before every commit
## Architecture
### Directory structure
```text
app/
├── api/ # FastAPI routers (endpoints)
├── core/ # Configuration, security, exceptions
├── models/ # SQLAlchemy models
├── schemas/ # Pydantic schemas (request/response)
├── services/ # Business logic
├── repositories/ # Data access (queries)
└── utils/ # Shared utilities
Layers
- Routers call Services
- Services call Repositories
- Repositories call the DB
- NEVER does a Router access the DB directly
- NEVER does a Repository contain business logic
Patterns
- Repository Pattern for data access
- Dependency Injection via FastAPI Depends()
- Pydantic for input AND output validation
- Async/await for all I/O operations
Forbidden Patterns
- DO NOT use
anytype in Python (typing.Any) without justification in a comment - NO raw SQL queries — always use SQLAlchemy ORM
- NO print() for logging — use the logger configured in app.core.logging
- DO NOT hardcode URLs, ports, or credentials — use environment variables
- DO NOT import from tests/ in production code
- DO NOT commit .env files, credentials, or tokens
- DO NOT use datetime.now() — use datetime.now(UTC) for consistency
- DO NOT modify files in alembic/versions/ manually — use alembic revision
Testing
Requirements
- Minimum coverage: 80% (enforced in CI)
- Every endpoint has at least 1 happy path test and 1 error test
- Every service function has a unit test
- Integration tests for complete flows (auth → create → read)
Conventions
- Test files: test_[module].py in tests/
- Shared fixtures in conftest.py
- Factory pattern to create test objects
- No hardcoded data — use factories or fixtures
- Each test is independent — doesn't depend on execution order
Frameworks
- pytest as the runner
- httpx.AsyncClient for endpoint tests
- pytest-asyncio for async tests
- factory-boy for model factories
Git and Workflow
Commits
- Conventional Commits: type(scope): description
- Types: feat, fix, refactor, test, docs, chore, ci
- Example: feat(auth): add JWT refresh token endpoint
- Messages in English, imperative, no final period
- Maximum 72 characters on the first line
Branches
- main: production, always deployable
- develop: integration
- feature/xxx: new features
- fix/xxx: bug fixes
- NEVER force push to main or develop
Pull Requests
- Requires at least 1 review
- CI must pass (tests + lint)
- Description includes: what, why, and how to test
Security
- NEVER commit .env, API keys, tokens, or passwords
- Sensitive variables go in environment variables
- Passwords are hashed with bcrypt (never in plain text)
- JWT secrets at least 256 bits
- Rate limiting active on all public endpoints
- Input validation with Pydantic on ALL endpoints
- Don't expose stack traces in error responses (use custom exceptions)
Dependencies
- Evaluate before adding: is the extra complexity worth it?
- Prefer stdlib over third-party when viable
- Pin versions in requirements.txt (==, not >=)
- Audit dependencies with
pip-auditmonthly - Don't install development packages in production
### Key points of the template
1. **Context first** — The agent needs to understand what the project is before the rules
2. **Conventions are specific** — "Type hints required" not "try to use type hints"
3. **Prohibitions are absolute** — "DO NOT" in uppercase, no ambiguity
4. **Testing is enforceable** — Quantifiable minimum coverage, not "try to test"
5. **Security is non-negotiable** — Security rules have no exceptions
---
## Merge Hierarchy
### The three levels of CLAUDE.md
Claude Code looks for and combines CLAUDE.md from multiple locations:
Level 1: Global (user) ~/.claude/CLAUDE.md → Personal preferences that apply to ALL your projects → E.g.: "Always respond in Spanish", "I prefer concise explanations"
Level 2: Project (repo root) ./CLAUDE.md → Team standard for this project → Gets committed to the repo → everyone shares it → E.g.: "TypeScript strict", "Conventional commits"
Level 3: Subdirectory (module-specific) ./src/api/CLAUDE.md → Rules specific to a project module → E.g.: "Endpoints follow RESTful naming", "Validation with Pydantic"
### Precedence rule
**More specific wins.** If there's a conflict between levels:
Global says: "Use tabs" Project says: "Use spaces (4)" Subdirectory says: "Use spaces (2)"
Result when you work in ./src/api/: → Use spaces (2) — subdirectory wins
Result when you work in ./src/models/: → Use spaces (4) — project wins (no subdirectory CLAUDE.md)
Result in another project without CLAUDE.md: → Use tabs — global wins (only level available)
### Merge in practice
Claude Code doesn't replace levels — it **combines** them. The rules from all applicable levels come together, and only those that explicitly conflict are overridden:
Global CLAUDE.md:
- Respond in Spanish
- Prefer concise explanations
- Use git conventional commits
Project CLAUDE.md:
- TypeScript strict
- Tests with vitest
- No console.log
When Claude Code works on this project, it follows EVERYTHING: ✅ Respond in Spanish (global) ✅ Concise explanations (global) ✅ Conventional commits (global) ✅ TypeScript strict (project) ✅ Tests with vitest (project) ✅ No console.log (project)
### CLAUDE.md in subdirectories
For large projects, you can have CLAUDE.md in subdirectories with module-specific rules:
my-project/ ├── CLAUDE.md ← Project rules ├── src/ │ ├── api/ │ │ ├── CLAUDE.md ← API-specific rules │ │ └── routes.py │ ├── models/ │ │ ├── CLAUDE.md ← Model-specific rules │ │ └── user.py │ └── utils/ │ └── helpers.py ← Only project rules (no subdir CLAUDE.md)
`src/api/CLAUDE.md`:
```markdown
## API-Specific Rules
- Endpoints use kebab-case URLs
- Every endpoint returns a Pydantic ResponseModel
- Error responses use app.core.exceptions, not raw HTTPException
- New endpoints require OpenAPI documentation
src/models/CLAUDE.md:
## Model-Specific Rules
- Every model has created_at and updated_at timestamps
- Soft delete with is_deleted flag, never hard delete
- Relationships use lazy="selectin" for async compatibility
- Migrations go through alembic, never manual schema changes
Enforcement: Hooks That Validate Compliance
The problem of "rules without teeth"
A CLAUDE.md without enforcement is a suggestion. Claude Code respects it most of the time, but LLMs can make mistakes. Enforcement with hooks turns suggestions into rules:
Without enforcement:
CLAUDE.md says "use type hints" →
Claude forgets in 3 of 20 functions →
Code review catches it (maybe)
With enforcement:
CLAUDE.md says "use type hints" →
PostToolUse hook runs mypy after each edit →
Claude receives the error and fixes it immediately →
100% compliance
Hook: Validate type hints after edit
scripts/hooks/enforce-type-hints.sh:
#!/bin/bash
INPUT=$(cat -)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')
if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
exit 0
fi
if [[ "$FILE" != *.py ]]; then
exit 0
fi
if command -v mypy &> /dev/null; then
MYPY_RESULT=$(mypy "$FILE" --ignore-missing-imports --no-error-summary 2>&1)
MYPY_EXIT=$?
if [ $MYPY_EXIT -ne 0 ]; then
MISSING_HINTS=$(echo "$MYPY_RESULT" | grep -c "missing return type\|no type annotation")
if [ "$MISSING_HINTS" -gt 0 ]; then
echo "CLAUDE.md VIOLATION: Missing type hints in $FILE"
echo "$MYPY_RESULT" | grep "missing return type\|no type annotation" | head -5
exit 1
fi
fi
fi
exit 0
Hook: Validate that forbidden patterns aren't used
scripts/hooks/enforce-forbidden-patterns.sh:
#!/bin/bash
INPUT=$(cat -)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')
if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
exit 0
fi
if [[ "$FILE" == *.py ]]; then
if grep -n "print(" "$FILE" | grep -v "^#" | grep -v "# noqa" > /dev/null 2>&1; then
LINES=$(grep -n "print(" "$FILE" | grep -v "^#" | grep -v "# noqa" | head -5)
echo "CLAUDE.md VIOLATION: print() found in $FILE"
echo "Use logger instead. Found:"
echo "$LINES"
exit 1
fi
if grep -n "datetime.now()" "$FILE" | grep -v "now(UTC)" > /dev/null 2>&1; then
echo "CLAUDE.md VIOLATION: datetime.now() without UTC in $FILE"
echo "Use datetime.now(UTC) for timezone consistency"
exit 1
fi
if grep -n "typing.Any" "$FILE" > /dev/null 2>&1; then
ANY_COUNT=$(grep -c "typing.Any\|from typing import.*Any" "$FILE")
JUSTIFIED=$(grep -c "# justified:" "$FILE")
if [ "$ANY_COUNT" -gt "$JUSTIFIED" ]; then
echo "CLAUDE.md VIOLATION: typing.Any without justification in $FILE"
echo "Add '# justified: reason' comment for each use of Any"
exit 1
fi
fi
fi
if [[ "$FILE" == *.ts ]] || [[ "$FILE" == *.tsx ]]; then
if grep -n "console\.log\|console\.warn\|console\.error" "$FILE" | grep -v "// debug" > /dev/null 2>&1; then
echo "CLAUDE.md VIOLATION: console.log found in $FILE"
echo "Use the project logger instead"
exit 1
fi
if grep -n ": any" "$FILE" | grep -v "// justified" > /dev/null 2>&1; then
echo "CLAUDE.md VIOLATION: 'any' type found in $FILE"
exit 1
fi
fi
exit 0
Hook: Validate naming conventions
scripts/hooks/enforce-naming.sh:
#!/bin/bash
INPUT=$(cat -)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')
if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
exit 0
fi
BASENAME=$(basename "$FILE")
if [[ "$FILE" == *.py ]]; then
if ! echo "$BASENAME" | grep -qE "^[a-z][a-z0-9_]*\.py$"; then
echo "CLAUDE.md VIOLATION: Python filename must be snake_case"
echo "File: $BASENAME"
echo "Expected: snake_case.py"
exit 1
fi
fi
if [[ "$FILE" == *.ts ]] || [[ "$FILE" == *.tsx ]]; then
if echo "$BASENAME" | grep -qE "^[A-Z]"; then
: # PascalCase components are OK in React
elif ! echo "$BASENAME" | grep -qE "^[a-z][a-z0-9-]*\.(ts|tsx)$"; then
echo "CLAUDE.md VIOLATION: TypeScript filename must be kebab-case"
echo "File: $BASENAME"
exit 1
fi
fi
exit 0
settings.json with all enforcement hooks
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "./scripts/hooks/enforce-forbidden-patterns.sh"
},
{
"type": "command",
"command": "./scripts/hooks/enforce-naming.sh"
},
{
"type": "command",
"command": "./scripts/hooks/enforce-type-hints.sh"
}
]
}
]
}
}
Onboarding: New Members Adopt the Standard Automatically
The ideal flow
New developer joins the team:
1. git clone repo-url → Gets CLAUDE.md + settings.json + hook scripts
2. Opens Claude Code → SessionStart hook verifies environment
3. Writes code → Hooks validate compliance in real time
4. Commits → Conventional commits, lint passes
5. Creates PR → CI validates coverage, linting, types
Result: consistent code from the very first commit.
No 20-page documents. No "how to configure" meetings.
Onboarding checklist with CLAUDE.md
What your repository needs for automatic onboarding:
✅ CLAUDE.md in the repo root (team constitution)
✅ .claude/settings.json with enforcement hooks
✅ scripts/hooks/ with all validation scripts
✅ pyproject.toml / .eslintrc configured (project linter)
✅ .github/CONTRIBUTING.md with a link to CLAUDE.md
✅ All hook scripts with chmod +x (executable)
Onboarding verification script
scripts/verify-setup.sh:
#!/bin/bash
echo "=== Team Setup Verification ==="
ERRORS=0
if [ ! -f "CLAUDE.md" ]; then
echo "❌ CLAUDE.md not found in the root"
ERRORS=$((ERRORS + 1))
else
echo "✅ CLAUDE.md present"
fi
if [ ! -f ".claude/settings.json" ]; then
echo "❌ .claude/settings.json not found"
ERRORS=$((ERRORS + 1))
else
if jq -e '.hooks' .claude/settings.json > /dev/null 2>&1; then
echo "✅ settings.json with hooks configured"
else
echo "⚠️ settings.json without hooks"
fi
fi
if [ -d "scripts/hooks" ]; then
HOOK_COUNT=$(ls scripts/hooks/*.sh 2>/dev/null | wc -l | tr -d ' ')
EXECUTABLE=$(find scripts/hooks/ -name "*.sh" -perm +111 2>/dev/null | wc -l | tr -d ' ')
echo "✅ $HOOK_COUNT hook scripts ($EXECUTABLE executable)"
if [ "$HOOK_COUNT" -ne "$EXECUTABLE" ]; then
echo "⚠️ Some scripts are not executable"
echo " Fix: chmod +x scripts/hooks/*.sh"
fi
else
echo "❌ scripts/hooks/ not found"
ERRORS=$((ERRORS + 1))
fi
if command -v ruff &> /dev/null; then
echo "✅ ruff installed"
elif command -v npx &> /dev/null && npx eslint --version > /dev/null 2>&1; then
echo "✅ eslint installed"
else
echo "⚠️ No linter found (ruff or eslint)"
fi
if command -v jq &> /dev/null; then
echo "✅ jq installed"
else
echo "❌ jq not installed (required by hooks)"
ERRORS=$((ERRORS + 1))
fi
echo ""
if [ $ERRORS -eq 0 ]; then
echo "✅ Setup complete. You're ready to work."
else
echo "❌ $ERRORS problems found. Review the errors above."
fi
exit $ERRORS
Maintaining the CLAUDE.md
When to update
| Signal | Action |
|---|---|
| New framework adopted | Add to "Code Conventions" |
| Recurring bug in PRs | Add to "Forbidden Patterns" |
| New architectural decision | Add to "Architecture" |
| Quarterly review | Verify everything is still current |
| New member reports confusion | Clarify the ambiguous section |
Update process
1. Create branch: git checkout -b update/claude-md-conventions
2. Edit CLAUDE.md
3. Update hooks if the new rules need enforcement
4. Create a PR with a description of the change
5. Team review (at least 2 approvals for constitution changes)
6. Merge to main
7. All members get the changes with git pull
CLAUDE.md anti-patterns
❌ CLAUDE.md of 500+ lines
→ Too long. Claude Code can lose instructions.
→ Keep it < 200 lines. Use subdirectory CLAUDE.md for details.
❌ Vague rules: "Write good code"
→ Not actionable. What is "good"?
→ Specific: "Functions of at most 30 lines"
❌ Rules that are never reviewed
→ They accumulate and contradict.
→ Quarterly review with the team.
❌ Rules without enforcement
→ They get ignored gradually.
→ If it's important, add a hook that validates it.
❌ Not allowing personal overrides
→ Frustrates experienced developers.
→ Use the hierarchy: team sets minimums, personal can be stricter.
Troubleshooting
"Claude Code ignores some CLAUDE.md rules"
Cause: The CLAUDE.md is too long or the rules are ambiguous. LLMs can lose instructions in long documents.
Solution: Keep the CLAUDE.md concise (< 200 lines). Prioritize the most important rules at the start of the file. Use enforcement hooks for critical rules:
wc -l CLAUDE.md
# If > 200, refactor: move details to subdirectory CLAUDE.md
"Conflict between global and project CLAUDE.md"
Cause: Your global CLAUDE.md says something that contradicts the project's.
Solution: The rule is clear: project wins over global. If your global says "use tabs" but the project says "use spaces", the project prevails. If the conflict is frequent, adjust your global to be more generic:
# ~/.claude/CLAUDE.md (global) — GOOD
Respond in Spanish.
Concise explanations.
Conventional commits.
# ~/.claude/CLAUDE.md (global) — BAD
Use tabs for indentation. ← This will conflict with projects that use spaces
Always use React. ← Doesn't apply to Python projects
Tests with Jest. ← Doesn't apply to all projects
"The enforcement hooks are too slow"
Cause: Hooks like mypy or eslint on large files take several seconds, and they run on every edit.
Solution: Make the hooks incremental — they only check the edited file, not the whole project:
# BAD: checks the whole project
mypy src/
# GOOD: checks only the edited file
mypy "$FILE" --ignore-missing-imports
"A developer needs an exception to the rule"
Cause: There's a legitimate case where the rule doesn't apply.
Solution: Use exception comments that the hooks recognize:
from typing import Any # justified: third-party lib returns untyped data
print("Debug info") # noqa: debugging, remove before merge
Configure the hooks to respect these markers (as shown in enforce-forbidden-patterns.sh).
"A subdirectory's CLAUDE.md contradicts the project's"
Cause: Conflicting rules between levels.
Solution: The subdirectory wins for files inside it. If the conflict is a mistake, resolve it in a PR. If it's intentional, document why:
# src/legacy/CLAUDE.md
## Exceptions for legacy code
- typing.Any allowed without justification (pre-typing era code)
- Minimum coverage: 50% (not 80% like the rest of the project)
- Reason: module in the process of migration, don't invest in full tests
Comparison: Rigid CLAUDE.md vs Flexible with Overrides
| Aspect | Rigid (project only) | Flexible (with personal overrides) |
|---|---|---|
| Setup | Simple: a single CLAUDE.md | More complex: multiple levels |
| Consistency | Maximum: everyone follows exactly the same rules | High: same base, controlled variations |
| Team satisfaction | Can frustrate experienced developers | High: respects personal preferences |
| Onboarding | Very simple: clone = ready | Simple: clone = base, customize = optional |
| Maintenance | Low: one file | Medium: watch that overrides don't break the standard |
| Enforcement | Direct: hooks validate against a single document | Needs understanding of the hierarchy |
| Ideal for | Small teams, critical projects | Medium+ teams, diverse experience |
| Risk | Too much rigidity → resistance | Too much flexibility → inconsistency |
Recommendation: Start with a project CLAUDE.md (rigid). When the team grows or developers experience friction, introduce personal overrides with the merge hierarchy. Keep security and architecture rules without override; allow overrides on style preferences.
Exercises
Exercise 1: Minimum viable CLAUDE.md (Easy)
Write a team CLAUDE.md with exactly 5 rules covering: language, naming, testing, one prohibition, and one git convention.
See solution
# CLAUDE.md — My Project
## Conventions
- Python 3.12 with type hints required on all functions
- snake_case for files and functions, PascalCase for classes
## Testing
- pytest for all tests, minimum coverage 80%
## Forbidden
- DO NOT use print() for logging — use structlog
## Git
- Conventional commits: type(scope): description in English
Five clear, specific, and actionable rules. An agent reading this knows exactly what to do.
Exercise 2: Merge hierarchy (Easy)
Given these three CLAUDE.md files, which rules does Claude Code follow when editing src/api/routes.py?
Global (~/.claude/CLAUDE.md):
- Respond in Spanish
- Use tabs
Project (./CLAUDE.md):
- Use spaces (4)
- Tests with pytest
Subdirectory (./src/api/CLAUDE.md):
- Endpoints return ResponseModel
- Use spaces (2)
See solution
For src/api/routes.py, Claude Code follows:
- Respond in Spanish ← Global (doesn't conflict with anything)
- Use spaces (2) ← Subdirectory (overrides project which says 4, which overrides global which says tabs)
- Tests with pytest ← Project (doesn't conflict)
- Endpoints return ResponseModel ← Subdirectory (additional rule)
The global's "Use tabs" rule is completely overridden by the project's "Use spaces (4)", which in turn is overridden by the subdirectory's "Use spaces (2)".
If Claude edits a file in src/models/ (without a subdirectory CLAUDE.md), it would use the project's spaces (4).
Exercise 3: Enforcement hook for imports (Medium)
Create a PostToolUse hook that validates that imports in Python files follow the correct order: stdlib → third-party → local. Report a violation (exit 1) if the order is incorrect.
See solution
scripts/hooks/enforce-import-order.sh:
#!/bin/bash
INPUT=$(cat -)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')
if [ -z "$FILE" ] || [ ! -f "$FILE" ] || [[ "$FILE" != *.py ]]; then
exit 0
fi
if command -v ruff &> /dev/null; then
IMPORT_CHECK=$(ruff check "$FILE" --select I --no-fix 2>&1)
if [ $? -ne 0 ]; then
echo "CLAUDE.md VIOLATION: Import order incorrect in $FILE"
echo "Expected: stdlib > third-party > local"
echo "$IMPORT_CHECK" | head -5
echo ""
echo "Fix: ruff check --select I --fix $FILE"
exit 1
fi
fi
exit 0
This hook uses ruff with the I (isort) rule to validate import order. If ruff isn't available, the hook passes silently (exit 0).
Exercise 4: CLAUDE.md with subdirectories (Medium)
Design a CLAUDE.md structure for a fullstack project with frontend/ (React + TypeScript) and backend/ (Python + FastAPI). The root CLAUDE.md defines common rules, and each subdirectory defines rules specific to its stack.
See solution
./CLAUDE.md (root):
# CLAUDE.md — FullStack App
## General
- Git conventional commits in English
- Don't hardcode URLs or credentials
- Sensitive variables in .env (never commit .env)
## Code Quality
- Minimum test coverage: 80%
- No TODO without an associated issue: "TODO(#123): description"
- Functions of at most 30 lines
./frontend/CLAUDE.md:
## Frontend Rules
- TypeScript strict mode, no `any`
- React functional components only (no class components)
- Styling with Tailwind CSS utility classes
- Tests with Vitest + React Testing Library
- File naming: PascalCase for components, kebab-case for utils
- Imports: react > third-party > @/components > @/utils > relative
./backend/CLAUDE.md:
## Backend Rules
- Python 3.12 with type hints on all functions
- FastAPI with async endpoints
- SQLAlchemy 2.0 async ORM (no raw SQL)
- Tests with pytest + httpx.AsyncClient
- File naming: snake_case
- Logging with structlog (no print())
- Pydantic v2 for all request/response schemas
Exercise 5: Complete enforcement system (Hard)
Create a settings.json that combines CLAUDE.md enforcement with approval flows: PostToolUse hooks validate CLAUDE.md rules, and PreToolUse hooks block commits that don't follow conventional commits.
See solution
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "./scripts/hooks/validate-commit-message.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "./scripts/hooks/enforce-forbidden-patterns.sh"
},
{
"type": "command",
"command": "./scripts/hooks/enforce-import-order.sh"
}
]
}
]
}
}
scripts/hooks/validate-commit-message.sh:
#!/bin/bash
INPUT=$(cat -)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if ! echo "$COMMAND" | grep -q "git commit"; then
exit 0
fi
COMMIT_MSG=$(echo "$COMMAND" | grep -oP '(?<=-m ")[^"]*' | head -1)
if [ -z "$COMMIT_MSG" ]; then
COMMIT_MSG=$(echo "$COMMAND" | grep -oP "(?<=-m ')[^']*" | head -1)
fi
if [ -z "$COMMIT_MSG" ]; then
exit 0
fi
PATTERN="^(feat|fix|refactor|test|docs|chore|ci|perf|style)\(.+\): .+"
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
echo "CLAUDE.md VIOLATION: Commit message must follow Conventional Commits"
echo "Format: type(scope): description"
echo "Types: feat, fix, refactor, test, docs, chore, ci, perf, style"
echo "Got: $COMMIT_MSG"
exit 2
fi
exit 0
Summary
- CLAUDE.md for teams is a constitution, not a config file — it defines behavior, values, and constraints that all agents respect
- An effective CLAUDE.md has clear sections: context, conventions, architecture, prohibitions, testing, git, security
- The merge hierarchy (global < project < subdirectory) allows shared rules with room for overrides in specific modules
- Enforcement with hooks turns CLAUDE.md rules into automatic validations: PostToolUse verifies compliance after each edit
- Automatic onboarding is the most tangible benefit: cloning the repo = adopting the standard, with no manual configuration
- Keep the CLAUDE.md concise (< 200 lines), specific (actionable rules), and up to date (quarterly review)
- Prohibitions are absolute and specific — "DO NOT use print()" not "avoid print() if you can"
- Enforcement hooks respect exception markers (
# justified:,# noqa:) for legitimate cases - Subdirectory CLAUDE.md allows module-specific rules without overloading the root CLAUDE.md
Additional Resources
- Claude Code CLAUDE.md Memory — Official documentation of CLAUDE.md, levels, and merge
- Claude Code Settings — settings.json configuration for enforcement
- Claude Code Hooks — PostToolUse hooks for compliance validation
- Claude Code Best Practices — CLAUDE.md best practices
- Claude Code Tips and Tricks — Tips for effective CLAUDE.md files
- Conventional Commits — Conventional commits specification
- ruff — Python Linter — Linter and formatter for Python used in enforcement
- Google Python Style Guide — Style reference that complements CLAUDE.md
Next capsule: In capsule 05 (Project) you'll build a complete team setup integrating everything you've learned: a production-ready CLAUDE.md for a 3-person team, remote control configured, approval flows for destructive operations, enforcement hooks, and an onboarding checklist. The complete setup you'll carry into the Module 8 capstone project.