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

  1. It gets committed to the repo → Everyone has it automatically
  2. It's declarative → "what to do", not "how to configure"
  3. It's evolutionary → It updates via PRs like any other file
  4. It's enforceable → Hooks can validate compliance
  5. 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 any type 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-audit monthly
  • 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

SignalAction
New framework adoptedAdd to "Code Conventions"
Recurring bug in PRsAdd to "Forbidden Patterns"
New architectural decisionAdd to "Architecture"
Quarterly reviewVerify everything is still current
New member reports confusionClarify 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

AspectRigid (project only)Flexible (with personal overrides)
SetupSimple: a single CLAUDE.mdMore complex: multiple levels
ConsistencyMaximum: everyone follows exactly the same rulesHigh: same base, controlled variations
Team satisfactionCan frustrate experienced developersHigh: respects personal preferences
OnboardingVery simple: clone = readySimple: clone = base, customize = optional
MaintenanceLow: one fileMedium: watch that overrides don't break the standard
EnforcementDirect: hooks validate against a single documentNeeds understanding of the hierarchy
Ideal forSmall teams, critical projectsMedium+ teams, diverse experience
RiskToo much rigidity → resistanceToo 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:

  1. Respond in Spanish ← Global (doesn't conflict with anything)
  2. Use spaces (2) ← Subdirectory (overrides project which says 4, which overrides global which says tabs)
  3. Tests with pytest ← Project (doesn't conflict)
  4. 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

  1. Claude Code CLAUDE.md Memory — Official documentation of CLAUDE.md, levels, and merge
  2. Claude Code Settings — settings.json configuration for enforcement
  3. Claude Code Hooks — PostToolUse hooks for compliance validation
  4. Claude Code Best Practices — CLAUDE.md best practices
  5. Claude Code Tips and Tricks — Tips for effective CLAUDE.md files
  6. Conventional Commits — Conventional commits specification
  7. ruff — Python Linter — Linter and formatter for Python used in enforcement
  8. 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.