Compositions
CLAUDE.md instructions are advisory — Claude can deprioritize or ignore them. Hooks are deterministic — they execute regardless of what Claude decides. The entire composition framework rests on this distinction: CLAUDE.md for guidance, hooks for guarantees.
CLAUDE.md + Hooks
Hooks transform CLAUDE.md from "please follow these rules" into an enforcement layer with real consequences. The two systems complement each other at every stage of a session.
Re-inject Context After Compaction
Compaction strips subdirectory CLAUDE.md files and skill descriptions from context. A SessionStart hook with a compact matcher re-injects critical instructions that would otherwise disappear:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use pnpm, not npm. Run tests before committing. Current sprint: auth refactor.'"
}
]
}
]
}
}You can also embed compaction instructions directly in CLAUDE.md:
When compacting, always preserve the full list of modified files and any test commands.Both approaches work. The hook is more reliable because it executes deterministically rather than depending on Claude reading and following the instruction during compaction.
The InstructionsLoaded Hook
The InstructionsLoaded hook fires when any CLAUDE.md or .claude/rules/*.md file loads into context. Matchers filter by load reason:
| Matcher | When It Fires |
|---|---|
session_start | Initial session load |
nested_traversal | Lazy-loaded subdirectory CLAUDE.md |
path_glob_match | Path-scoped rule triggered by file access |
include | @import expansion |
compact | Re-injection after compaction |
Example — log which instruction files load and when:
{
"hooks": {
"InstructionsLoaded": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "jq -c '{file: .file_path, reason: .reason}' >> ~/.claude/instruction-loads.log"
}
]
}
]
}
}This log reveals exactly which instructions are active at any point in a session — invaluable for debugging why Claude followed one rule but ignored another.
Enforcing What CLAUDE.md Cannot
CLAUDE.md says "never edit migration files." Claude might still do it. A PreToolUse hook guarantees it:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/protect-files.sh"
}
]
}
]
}
}The hook script examines the file path in tool_input, checks it against protected patterns, and exits with code 2 to block the operation. The reason printed to stderr becomes Claude's feedback, so it adjusts its approach.
A minimal protection script:
#!/bin/bash
FILE_PATH=$(jq -r '.tool_input.file_path' <<< "$CLAUDE_TOOL_INPUT")
if [[ "$FILE_PATH" == *"/migrations/"* ]]; then
echo "BLOCKED: Migration files are immutable. Create a new migration instead." >&2
exit 2
fiAuto-Format After Edits
Instead of wasting CLAUDE.md lines on formatting rules (semicolons, quotes, indentation), use a PostToolUse hook:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null; true"
}
]
}
]
}
}This removes 5-10 lines of formatting instructions from your CLAUDE.md while achieving better compliance. Linters are deterministic; LLM-based formatting is not.
CLAUDE.md + Skills
Skills are markdown files in .claude/skills/ that teach Claude repeatable workflows. They load on demand rather than every session — the key distinction from CLAUDE.md.
The Decision Boundary
| Put it in CLAUDE.md | Make it a skill |
|---|---|
| Applies to every session | Only matters for specific workflows |
| Short (1-3 lines) | Multi-step procedure |
| Build commands, conventions | Deployment process, code review checklist |
| Architecture overview | Detailed API design guide |
Skills with disable-model-invocation: true in their frontmatter stay completely out of context until explicitly invoked with /name. Zero token cost until needed.
CLAUDE.md as a Skill Directory
Reference skills from CLAUDE.md so Claude knows they exist without loading their full content:
# Workflows
- For deploying, use /deploy (handles staging + production)
- For code review, use /review-component
- For new API endpoints, use /new-endpointThis costs 3 lines of CLAUDE.md (~30 tokens) instead of embedding entire workflow procedures that could cost 500+ tokens each.
Migrating Bloated CLAUDE.md to Skills
A common evolution path for projects where CLAUDE.md has grown past 200 lines:
- Identify multi-step procedures in your CLAUDE.md (deployment, onboarding, code review)
- Extract each into
.claude/skills/<name>.md - Replace the CLAUDE.md section with a one-line reference
- Add
disable-model-invocation: trueif it should only fire on explicit/nameinvocation
Before:
## Deployment Process
1. Run the full test suite with `pnpm test`
2. Build production bundle with `pnpm build`
3. Check bundle size with `pnpm analyze`
4. If bundle exceeds 100KB, investigate with source-map-explorer
5. Deploy to staging with `pnpm deploy:staging`
6. Run smoke tests against staging
7. Deploy to production with `pnpm deploy:prod`
8. Verify production health checks passAfter (in CLAUDE.md):
- For deploying, use /deployAfter (in .claude/skills/deploy.md):
---
name: deploy
description: Full deployment pipeline from test to production
disable-model-invocation: true
---
[Full deployment procedure here]CLAUDE.md + Auto Memory
Two complementary systems, both loaded at session start but serving different purposes.
| CLAUDE.md | Auto Memory | |
|---|---|---|
| Who writes it | You | Claude |
| What it contains | Instructions and rules | Learnings and patterns |
| Size limit | No hard limit (but under 200 lines recommended) | First 200 lines or 25KB of MEMORY.md |
| Survives compaction | Project root: yes. Subdirectory: reloads lazily | Re-read from disk on demand |
| Version controlled | Yes (project-level) | No (stored in ~/.claude/projects/) |
Auto memory stores in ~/.claude/projects/<project>/memory/MEMORY.md plus topic files. All worktrees and subdirectories within the same git repo share one auto memory directory.
When to Use Which
If you find yourself correcting Claude repeatedly in chat — "no, we use pnpm" — that correction should be in CLAUDE.md. If Claude discovers a project-specific pattern through work — "the auth middleware expects a Bearer token in the X-Custom-Auth header" — auto memory captures it automatically.
The overlap zone: when Claude keeps making the same mistake and auto memory doesn't capture the correction, add it to CLAUDE.md manually. The instruction is too important to leave to automatic discovery.
CLAUDE.md + Subagents
Subagents receive their own copy of CLAUDE.md, counted against the subagent's context window, not yours. This has specific implications for instruction architecture.
What Subagents Inherit
- Project-root CLAUDE.md: loaded fresh
- Ancestor CLAUDE.md files: loaded fresh
- CLAUDE.local.md: loaded fresh
What Subagents Do NOT Inherit
- Parent conversation's auto memory
- Skills from the parent (must be listed explicitly in
skillsfrontmatter) - Conversation context from the parent session
- The ability to spawn their own subagents (prevents recursion)
Built-in Explore and Plan agents skip CLAUDE.md loading entirely for a smaller context footprint.
Preloading Skills Into Subagents
---
name: api-developer
description: Implement API endpoints following team conventions
skills:
- api-conventions
- error-handling-patterns
---This gives the subagent access to specific skills without loading them into the parent session's context.
Writing Instructions for Subagents
Because subagents get their own copy of CLAUDE.md, instructions that reference "the current conversation" or "what we discussed earlier" will not work. Write instructions as self-contained directives:
# CLAUDE.md — subagent-safe instructions
## API Conventions
- All endpoints return `{ data, error, meta }` response envelope
- Use Zod schemas for request validation (see src/schemas/)
- Error responses follow RFC 7807 Problem Details format
## Test Requirements
- Every new endpoint needs integration tests in __tests__/
- Run `pnpm test:api` to verify (not the full suite)CLAUDE.md + MCP
MCP tool names load as deferred references at startup (~120 tokens). Full schemas load on demand when Claude uses tool search. CLAUDE.md can guide MCP tool selection:
# MCP Tools
- Use the Playwright MCP for UI testing (available via browser-tester subagent)
- Prefer `gh` CLI over GitHub MCP for simple operations (lower token cost)
- The Notion MCP is configured for the engineering wiki workspaceKeeping MCP Overhead Low
If your MCP servers define more than 20K tokens of tool definitions, performance degrades measurably. Two strategies:
- Use
ENABLE_TOOL_SEARCH=autoto defer schema loading until Claude needs a specific tool:
ENABLE_TOOL_SEARCH=auto claude- Define MCP servers inline in a subagent's
mcpServersfield rather than in.mcp.json— keeps them out of the main session entirely:
---
name: ui-tester
description: Run UI tests with Playwright MCP
mcpServers:
playwright:
command: npx
args: ["@anthropic/mcp-playwright"]
---
Run Playwright tests against the staging URL. Navigate to each page,
verify interactive elements work, and report failures with screenshots.The Full Stack
A production setup combining all systems:
project/
├── CLAUDE.md # 50-100 lines: build, architecture, conventions
├── CLAUDE.local.md # 10-20 lines: personal focus, local env
├── .claude/
│ ├── settings.json # Hooks for enforcement and automation
│ ├── rules/
│ │ ├── code-style.md # Unconditional style rules
│ │ └── api-safety.md # Path-scoped for src/api/**
│ ├── skills/
│ │ ├── deploy.md # /deploy workflow
│ │ ├── review.md # /review workflow
│ │ └── new-endpoint.md # /new-endpoint scaffold
│ └── hooks/
│ ├── protect-files.sh # Block edits to migrations
│ └── auto-format.sh # Prettier after every editThe settings.json tying this together:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/protect-files.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/auto-format.sh"
}
]
}
]
},
"permissions": {
"allow": [
"Bash(pnpm test:*)",
"Bash(pnpm build)",
"Bash(pnpm typecheck)"
]
}
}CLAUDE.md provides the mental model. Hooks enforce the boundaries. Skills hold the procedures. Auto memory captures the learnings. Each system handles what it does best — no overlap, no gaps.