You write "never use doc.save() — always use findByIdAndUpdate()" in your CLAUDE.md. Claude follows it. For weeks, every Mongoose update in your codebase uses atomic operations. You start to trust it.
Then one Friday afternoon, you launch a subagent to handle a complex data migration. The subagent is working through a dozen files, juggling schema changes and data transforms. Buried in the eighth file, it quietly uses doc.save(). No fanfare. No explanation. Just a method call that introduces a race condition you won't find until Monday morning, after two users report corrupted data.
The rule was there. Claude just... didn't follow it.
This isn't a failure story. It's a calibration story. CLAUDE.md was the right tool for 95% of cases. For that last 5%, you needed something stronger.
Part 1 mapped the full extension stack. Now we're going deep on the three "internal brain" layers — CLAUDE.md, hooks, and skills — and the question that actually matters: when is each one trustworthy enough to bet on?
What is CLAUDE.md, and what goes in it?
CLAUDE.md is a markdown file at your project root that Claude reads at the start of every session. It loads automatically, requires no invocation, and shapes every action Claude takes. Think of it as your project's constitution: short, opinionated, non-negotiable. The best ones are under 80 lines — everything else goes in scoped rules or skills.
The analogy I keep coming back to: it's the onboarding doc you'd hand a new developer on day one. Not the full documentation — just the critical stuff they need to not break things.
What belongs in CLAUDE.md
The best CLAUDE.md files share a pattern: they're short, structured, and opinionated. They answer five questions:
- What is this project? — Tech stack, architecture, key patterns.
- What are the hard rules? — Things that must never happen.
- How do I run things? — Build, test, deploy commands.
- What are the gotchas? — Non-obvious mistakes that cost time.
- Where do I look for more? — Links to detailed docs.
Here's a trimmed version of a real CLAUDE.md from a production NestJS monorepo:
# CLAUDE.md
## What Is This
NestJS + MongoDB + Firebase Auth monorepo. Port 3100.
All workflows go through Makefile targets — run `make help`.
## Hard Rules
- NO .env files (secrets via Doppler)
- NO `doc.save()` — always `findByIdAndUpdate()`
- NO `console.log` — use NestJS Logger
- NO direct DB queries without workspaceId scoping
- Never push to main (always PR)
## Quick Commands
```bash
make backend # Start all services
make dev-health # Check service health
make api-get ENDPOINT=/api/v1/agents # Test endpointsArchitecture
| Service | Port | Routes |
|---|---|---|
| platform-server | 3100 | /users/, /workspaces/ |
| agent-service | 8002 | /agents/, /tools/ |
| kb-service | 8990 | /knowledge/* |
Gotchas
- After git pull:
make rebuild-packages && make backend - Use
.idnot._idin JSON responses - 404/401/500 = FAILURE (never rationalize away)
Detailed Docs
Notice what's not there: no prose explanations, no edge case discussions, no implementation details. It's terse, scannable, and fits on a screen.
### The rough instruction ceiling
Here's the part that isn't in the documentation: in my experience, Claude can reliably follow roughly 150-200 total instructions across its system prompt and CLAUDE.md combined. This isn't a hard API limit — it's a practical observation from running this across hundreds of sessions. The system prompt already uses roughly 50 of those slots. That leaves you approximately 100-150 effective instructions before adherence starts dropping off.
"Instructions" doesn't mean lines of text. It means discrete rules Claude needs to track. "Use findByIdAndUpdate()" is one instruction. A paragraph explaining why, with three examples and two exceptions, is still roughly one instruction — but it's a fuzzy one that competes for attention with everything else.
When your CLAUDE.md grows past this threshold, something subtle happens. Claude doesn't throw an error. It doesn't warn you that it's ignoring rules. It just starts... occasionally forgetting some of them. Not the same ones every time. Not predictably. Just enough to make you question whether the rule exists at all.
I've seen CLAUDE.md files over 800 lines. The teams that wrote them thought they were being thorough. In practice, Claude followed maybe 60% of the instructions — and the team had no idea which 40% were being silently dropped.
### Scoped rules: the escape hatch
The solution to CLAUDE.md bloat isn't "write fewer rules." It's scoped rules. These are markdown files in `.claude/rules/` with frontmatter that tells Claude when to load them:
```markdown
---
globs:
- "services/**/*.ts"
- "packages/nestjs-common/**"
---
# Backend Services Rules
## Module Structure
Every service has an AppModule that imports shared modules:
- InfrastructureModule.forRoot() — Config + MongoDB + Redis
- AuthModule.forRoot() — Global auth guard
- HealthModule.forRoot() — Health endpoints
## Mongoose Patterns
- workspaceId on every schema, always indexed
- Use virtualIdPlugin for .id transformation
- findOneAndUpdate, never doc.save()
- Always scope queries by workspaceId
## DTO Rules
- class-validator + @ApiProperty on every field
- @ValidateNested() + @Type(() => NestedDto) for nested objects
- Extend PaginationDto for list endpointsWhen Claude opens a file matching services/**/*.ts, this rule file loads automatically. When it's working on a frontend component, it doesn't. The backend rules don't compete for attention during frontend work, and vice versa.
Here's a real directory of scoped rules from a production project:
.claude/rules/
backend-services.md # globs: services/**/*.ts
api-contracts.md # globs: *.controller.ts, *.dto.ts
frontend-apps.md # globs: apps/**/*.tsx
sdk-cli.md # globs: packages/platform-sdk/**
inter-service.md # globs: **/clients/**, **/main.ts
deploy-infra.md # globs: make/**, Dockerfile*, fly.toml
lessons-backend.md # globs: services/**
lessons-frontend.md # globs: apps/**
lessons-integration.md # globs: packages/**Each file can be 100-200 lines without bloating CLAUDE.md. And because they only load when relevant, you can have deep, specific guidance for every area of your codebase without hitting the instruction ceiling.
CLAUDE.md best practices
After running this setup across a 17-service monorepo for months, here's what I've learned:
Keep CLAUDE.md under 80 lines. Seriously. Use bullet points, tables, and code blocks. No prose paragraphs. If you need to explain why a rule exists, link to a doc — don't explain it inline.
Hard rules go first. Claude gives more weight to content near the top of CLAUDE.md. Your non-negotiable rules should be in the first 20 lines.
Link, don't inline. Instead of putting your full architecture documentation in CLAUDE.md, add a link: See [Architecture](docs/architecture/). Claude can read linked files when it needs the detail.
Tables beat prose. Compare "The agent-service runs on port 8002 and handles routes for agents and tools, while the kb-service runs on port 8990..." versus a table. Claude parses tables faster and more accurately.
Test your rules. Periodically ask Claude to do something that violates one of your CLAUDE.md rules. If it does it without hesitation, the rule isn't landing. Reword it, move it higher, or promote it to a hook.
How do Hooks give you 100% enforcement?
Hooks are shell scripts that fire on specific Claude Code events, and they're the only mechanism that operates at 100% reliability. They execute outside of Claude's decision loop — Claude doesn't interpret them, doesn't weigh them against context, doesn't skip them when it's busy. If the script exits with code 2, the action is hard-blocked. If it outputs a JSON asking for confirmation, the user gets prompted. Either way, Claude can't override it.
This makes hooks fundamentally different from everything else in the extension stack. CLAUDE.md is a suggestion. Skills are a playbook. Hooks are physics.
The event lifecycle
Claude Code defines several hook events:
| Event | When It Fires | Common Use |
|---|---|---|
PreToolUse | Before Claude runs any tool (bash, edit, write) | Block dangerous commands, force user confirmation |
PostToolUse | After a tool completes | Log actions, validate output |
PreCommit | Before git commit | Run tests, lint, validate |
Notification | When Claude sends a notification | Alert routing, logging |
Stop | When Claude finishes a turn | Cleanup, summary logging |
The critical one is PreToolUse. It fires before every bash command, every file edit, every write operation. And it can block the action entirely.
Real example: blocking dangerous commands
Here's a production hook that prevents Claude from running destructive operations without explicit permission:
#!/bin/bash
# .claude/hooks/directory-permissions.sh
# Blocks dangerous git and deploy commands
#
# Exit codes:
# 0 (no output) = allow the action
# 0 + JSON stdout with permissionDecision:"ask" = force user prompt
# 2 = hard block (cannot proceed even with user approval)
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# No command = not a bash tool call, allow
[ -z "$COMMAND" ] && exit 0
REASON=""
# Block direct pushes
if echo "$COMMAND" | grep -qE 'git\s+push'; then
REASON="Use /git-push skill instead of direct git push"
fi
# Block destructive git operations
if echo "$COMMAND" | grep -qE 'git\s+(reset\s+--hard|stash|rebase|clean\s+-f)'; then
REASON="Destructive git operation — requires explicit user approval"
fi
# Block direct deploys
if echo "$COMMAND" | grep -qE '(fly\s+deploy|vercel\s+(--prod|deploy))'; then
REASON="Use make deploy targets instead of direct CLI"
fi
# If we found a reason, force the user to confirm
if [ -n "$REASON" ]; then
echo "{\"permissionDecision\":\"ask\",\"reason\":\"$REASON\"}"
exit 0
fi
exit 0When Claude tries to run git push origin main, this hook catches it and forces a user confirmation prompt. Every time. No exceptions. No "but the context suggested..." No probabilistic drift. If you want a hard block instead of a prompt, use exit 2.
Real example: pre-commit test runner
#!/bin/bash
# .claude/hooks/pre-commit-tests.sh
# Runs relevant tests before allowing commits
INPUT=$(cat)
# Detect which services were modified
CHANGED=$(git diff --cached --name-only)
FAILED=0
# If backend services changed, run type checks
if echo "$CHANGED" | grep -q "services/"; then
echo "Running type check on backend services..." >&2
npx tsc --noEmit --project tsconfig.json 2>/dev/null
if [ $? -ne 0 ]; then
echo "TypeScript type check failed" >&2
FAILED=1
fi
fi
# If SDK changed, build it
if echo "$CHANGED" | grep -q "packages/platform-sdk/"; then
echo "Building SDK..." >&2
make build-sdk 2>/dev/null
if [ $? -ne 0 ]; then
echo "SDK build failed" >&2
FAILED=1
fi
fi
# Exit 2 = hard block, commit cannot proceed
if [ $FAILED -ne 0 ]; then
exit 2
fi
exit 0Configuring Hooks
Hooks are defined in .claude/settings.json. Each event type gets an array of matchers, and each matcher has its own hooks array:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/directory-permissions.sh"
}
]
}
],
"PreCommit": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/pre-commit-tests.sh"
}
]
}
]
}
}The matcher field controls which tool triggers the hook — "Bash" for bash commands only, "*" for everything. Hooks should be fast. A pre-commit hook running a full test suite at 5 minutes will grind your session to a halt. Keep PreToolUse hooks under 1 second. Keep PreCommit hooks under 30 seconds — run targeted checks, not the full suite.
The key insight: "please don't" vs. "you can't"
This is the mental model that changed how I think about Claude configuration:
CLAUDE.md says "please don't." It's a request. Claude weighs it against context, competing instructions, and the complexity of the current task. Most of the time it complies. Sometimes it doesn't.
Hooks say "you can't." They're a wall. Claude doesn't get to weigh them against anything. The shell script runs, and if it exits with code 2, the action is hard-blocked. If it outputs {"permissionDecision":"ask"}, the user gets prompted to confirm. Either way, Claude's opinion on the matter is irrelevant.
Map your rules accordingly. "Use Tailwind tokens instead of raw hex colors" is a "please don't" — put it in CLAUDE.md. "Never push to main without a PR" is a "you can't" — enforce it with a hook.
What are Skills, and when should you use them instead of rules?
Skills are markdown files that give Claude step-by-step procedures for specific tasks. They load on demand when you invoke them — /deploy, /commit, /review — and they're procedural rather than declarative. Rules say "never do X." Skills say "here's exactly how to do Y, step by step." If you've ever wished you could hand Claude a recipe instead of a rulebook, that's what skills are for.
When you type /deploy in Claude Code, Claude reads the skill file and follows the procedure. It's the difference between handing someone a policy manual and handing them a checklist.
Skill anatomy
Skills live in .claude/commands/ (for simple slash commands) or .claude/skills/<name>/SKILL.md (for richer skill definitions with allowed-tools). Here's a real deploy skill:
---
name: deploy
description: "Deploy services to staging or production with health checks and rollback"
allowed-tools: Bash(make:*), Bash(git:*), Bash(flyctl:*)
---
# Deploy Skill
## Usage
`/deploy <environment>` where environment is `staging` or `prod`
## Procedure
### Pre-deploy Checks
1. Run `make dev-health` to verify all local services are healthy
2. Run `git status` to ensure working tree is clean
3. Run `git log --oneline -5` to confirm the latest commits
4. Show the user what will be deployed and ASK for confirmation
### Deploy to Staging
1. Run `make deploy-staging`
2. Wait for deploy to complete (check output for errors)
3. Run `make dashboard-staging` to verify staging health
4. Report results to user
### Deploy to Production
1. REQUIRE explicit user confirmation with the phrase "yes, deploy to prod"
2. Run `make deploy-prod`
3. Run `make dashboard-prod` to verify production health
4. Post to #all-6fs Slack channel with deploy summary
5. Report results to user
### Rollback
If any health check fails after deploy:
1. Alert the user immediately
2. Show the failing health checks
3. Ask if they want to rollback
4. If yes: `fly releases -a chanl-platform` then deploy previous imageNotice the structure: a description that tells Claude when this skill is relevant, then explicit steps. No ambiguity about what to do or in what order.
Skills vs. CLAUDE.md: when to use which
The distinction comes down to one question: is this a fact or a procedure?
| Type | Goes In | Example |
|---|---|---|
| Fact / convention | CLAUDE.md or scoped rules | "Use findByIdAndUpdate, not doc.save()" |
| Procedure / workflow | Skill (.claude/commands/) | "To deploy: check health, confirm, run make deploy, verify" |
| Pattern to follow | Scoped rule (.claude/rules/) | "All controllers need @ApiTags and @ApiOperation" |
| Multi-step recipe | Skill | "To create a new service: scaffold, register routes, add health check..." |
If you put procedures in CLAUDE.md, they bloat it. If you put facts in skills, Claude only sees them when the skill is invoked. Match the content to the container.
Real example: a commit workflow skill
---
description: Review changes, write commit message, and commit
allowed-tools: Bash(git:*)
---
# Commit Skill
## Steps
1. **Diff Review**: Run `git diff --staged` and `git status` in parallel
2. **Analyze Changes**: Summarize what changed and why
3. **Recent History**: Run `git log --oneline -5` to match commit style
4. **Draft Message**: Write a conventional commit message:
- `feat:` for new features
- `fix:` for bug fixes
- `refactor:` for code changes that don't add features or fix bugs
- Focus on "why" not "what"
5. **Commit**: Create the commit using a HEREDOC for the message
6. **Verify**: Run `git status` to confirm clean state
## Rules
- NEVER amend existing commits unless explicitly asked
- NEVER use --no-verify
- If pre-commit hook fails, fix the issue and create a NEW commit
- Stage specific files, not `git add -A`This is a procedure Claude follows step by step. Putting these 20 lines of workflow in CLAUDE.md would waste instruction slots on something Claude only needs during commits.
Real example: a code review skill
---
name: review
description: "Review session work against the active plan"
allowed-tools: Bash(make:*), Bash(npx:*), Read
---
# Review Skill
## Procedure
1. **Load Context**: Read the active plan from the task list
2. **Check Each Task**: For every task in the plan:
- Is the implementation complete?
- Do the acceptance criteria pass?
- Were tests written/updated?
3. **Run Verification**:
- `make dev-health` if services were modified
- Type check: `npx tsc --noEmit`
- Run relevant use case tests
4. **Cross-Project Consistency**:
- Do SDK types match API response shapes?
- Do UI hooks use the correct SDK methods?
5. **Report**: Summary with verdict (COMPLETED / NEEDS WORK)The probabilistic nature of Skills
Skills have a reliability gap that's important to understand. Unlike hooks (100% enforcement), skills depend on Claude deciding to follow the procedure as written. Most of the time it does. But when the task is complex, when there are competing priorities, or when a subagent is executing the skill in a constrained context, steps can get skipped.
I've seen Claude skip step 4 ("ask for confirmation") in a deploy skill because the previous conversation context strongly implied the user wanted to deploy. Claude was being helpful. It was also being dangerous.
The mitigation: for high-stakes steps, pair the skill with a hook. The deploy skill says "ask for confirmation." The PreToolUse hook blocks fly deploy if it hasn't seen a confirmation pattern in the recent conversation. Belt and suspenders.
How reliable is each layer, really?
Different enforcement mechanisms have different reliability levels, and you need to design around that. No one tells you this upfront, but it's the single most important thing to understand about Claude configuration.
This isn't a precise measurement — it's calibrated from real-world usage across hundreds of sessions. The pattern is consistent: shorter, more specific, more prominently placed instructions are followed more reliably.
Mapping rules to the right layer
Use this decision framework:
Safety and security rules? Hooks. If violating the rule could cause data loss, security breaches, or production incidents, it needs 100% enforcement. Hook it. "Never push to main" is a hook. "Never deploy without confirmation" is a hook. "Never run DROP TABLE" is absolutely a hook.
Coding patterns and conventions? CLAUDE.md or scoped rules. "Use findByIdAndUpdate" is a CLAUDE.md rule. It's fine at ~85% reliability because the 15% failure case is a code review catch, not a production incident. "Every DTO field needs @ApiProperty" goes in a scoped rule for *.dto.ts files. (For managing the prompts and instructions your production agents use, the same principle applies — critical constraints get enforced, conventions get suggested.)
Complex multi-step workflows? Skills. Deploy procedures, commit workflows, code review checklists — anything with more than 3 sequential steps is a skill. Claude follows procedures better when they're structured as explicit numbered steps rather than embedded in a wall of rules.
Simple preferences? CLAUDE.md. "Use tabular-nums on numbers" and "max 3 font weights" are fine as CLAUDE.md rules. If Claude occasionally uses font-bold instead of font-semibold, the world doesn't end.
What you should never do is put a safety rule only in CLAUDE.md and trust it. If "never expose API keys in logs" is only a CLAUDE.md instruction, you're one complex debugging session away from a subagent printing credentials to the terminal. Hook it.
What does a production .claude/ directory look like?
Here's the actual directory structure from a 17-service NestJS monorepo that's been running this setup for months. Yours will look different, but the shape is the same: settings for hooks, commands for skills, rules for context-specific guidance.
.claude/
settings.json # Hooks, permissions, preferences
commands/ # Slash-command skills (/deploy, /commit, /review)
deploy.md
commit.md
review.md
plan.md
board.md
rules/ # Scoped rules (auto-loaded by file pattern)
backend-services.md # globs: services/**/*.ts
api-contracts.md # globs: *.controller.ts, *.dto.ts
frontend-apps.md # globs: apps/**/*.tsx
sdk-cli.md # globs: packages/platform-sdk/**
deploy-infra.md # globs: make/**, Dockerfile*
lessons-backend.md # globs: services/**
lessons-frontend.md # globs: apps/**
hooks/ # Shell scripts for deterministic enforcement
directory-permissions.sh
pre-commit-tests.sh
CLAUDE.md # Top-level constitution (root of repo)The hierarchy: user, project, workspace
Configuration loads in layers, with each level overriding the previous:
| Level | Location | Scope | Committed to Git? |
|---|---|---|---|
| User | ~/.claude/ | All projects | No |
| Project | .claude/ | This repo | Yes |
| Workspace | .claude/ in workspace root | Multi-root workspaces | Yes |
User-level (~/.claude/CLAUDE.md, ~/.claude/settings.json): Personal preferences that apply everywhere. Your preferred commit style, global hooks, tools you always want available. This is not committed to version control.
Project-level (.claude/ in repo root): Team conventions committed to git. When someone clones the repo, they get the same rules, skills, and hooks. This is where 90% of your configuration lives.
CLAUDE.md: Can exist at both levels. The project CLAUDE.md contains repo-specific rules. A user-level CLAUDE.md might contain "always use vim keybindings" or "prefer concise explanations."
The key design decision: everything in .claude/ gets committed. Every team member runs with the same guardrails. Personal preferences stay in ~/.claude/. If someone on the team likes to use emoji in commit messages, that's their ~/.claude/CLAUDE.md — not the project's.
settings.json: the full picture
Here's a realistic production settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/directory-permissions.sh"
}
]
}
],
"PreCommit": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/pre-commit-tests.sh"
}
]
}
]
},
"permissions": {
"allow": [
"Bash(make *)",
"Bash(git status)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(npm run *)",
"Bash(npx tsc *)"
],
"deny": [
"Bash(rm -rf /)",
"Bash(curl * | bash)"
]
}
}The permissions block is a secondary defense layer. allow lists commands Claude can run without asking. deny blocks commands outright. But here's a subtlety: hooks run at a different point in the pipeline than permissions. If you need true enforcement, hooks are the answer. Permissions are a convenience layer for reducing confirmation prompts, not a security boundary.
What are the most common configuration mistakes?
These all cost me real debugging time. Every one of them felt obvious in hindsight.
Gotcha 1: CLAUDE.md over 500 lines
I once had a CLAUDE.md that was 500+ lines, covering every convention, every pattern, every gotcha for a 17-service monorepo. It felt comprehensive. In practice, Claude followed about 60% of the rules. The other 40% varied randomly between sessions.
The fix: I extracted everything into scoped rules and skills. CLAUDE.md dropped to 70 lines. Hard rules went to the top. Everything else moved to .claude/rules/ with appropriate globs. Rule adherence jumped from 60% to around 90%.
The lesson: CLAUDE.md isn't a wiki. It's a constitution. Keep it short enough that every line matters.
Gotcha 2: Skills without clear descriptions
My first skills had descriptions like "Deploy helper" and "Commit tool." Claude rarely invoked them. When I changed the descriptions to "Deploy services to staging or production with health checks and rollback" and "Review staged changes, write conventional commit message, verify clean state," invocation rates doubled.
Claude uses the description to decide relevance. Vague descriptions mean the skill competes poorly against Claude's built-in reasoning. Be specific about what the skill does and when it's useful.
Gotcha 3: Hooks that are too slow
I wrote a PreToolUse hook that ran a full security scan on every bash command. It took 3-4 seconds per command. In a session where Claude runs 50+ commands, that added 2-3 minutes of dead time. The workflow felt sluggish and unresponsive.
The rule: PreToolUse hooks must finish in under 1 second. If you need a longer check, move it to PreCommit (which runs less frequently) or run it asynchronously and report results.
Gotcha 4: rules about tools in CLAUDE.md
I had rules in CLAUDE.md about how to use specific tools and MCP servers. "When using the deploy tool, always check staging first." Claude followed this maybe 60% of the time because the instruction was disconnected from the context where it mattered.
The fix: tool-specific instructions go in skills, not CLAUDE.md. The /deploy skill includes "check staging first" as step 1. When Claude is executing the deploy skill, that instruction is right in front of it, not competing with 100 other rules.
Gotcha 5: not using scoped rules for context-specific guidance
Early on, I had frontend CSS rules in the main CLAUDE.md: "use semantic tokens, not raw colors" and "8pt grid spacing." These rules loaded during backend work too, wasting instruction slots. Worse, the backend-specific rules sometimes got deprioritized because of the frontend noise.
Scoped rules solved this completely. Frontend rules only load when touching .tsx files. Backend rules only load when touching service .ts files. Each context gets full attention to its specific rules without cross-contamination.
Gotcha 6: trusting CLAUDE.md for subagent safety
The most dangerous gotcha: assuming subagents inherit CLAUDE.md with the same fidelity as the main thread. They don't. Subagents operate in constrained contexts with their own instruction budgets. A rule that's reliably followed in the main thread can be dropped by a subagent working under heavy context pressure.
For anything safety-critical that subagents might encounter, use hooks. The hook runs regardless of which thread triggers the action.
Putting it all together
The three layers complement each other because they're good at different things. CLAUDE.md handles breadth, hooks handle enforcement, and skills handle procedural complexity. Here's the full picture:
| Layer | Reliability | Loaded | Content Type | Cost |
|---|---|---|---|---|
| CLAUDE.md | ~70-90% | Always | Facts, conventions | ~30-50 tokens per rule |
| Scoped Rules | ~80-85% | On file match | Deep patterns, gotchas | ~30-50 tokens per rule |
| Skills | ~75-85% | On invocation | Procedures, workflows | ~30-50 tokens per skill |
| Hooks | 100% | On event | Enforcement, safety | Shell script execution time |
The design principle: trust each layer for what it's good at, and compensate for what it's not.
CLAUDE.md handles breadth — the 100 conventions your team agrees on. Most are followed. The ones that aren't get caught in code review by humans who know the conventions too.
Hooks handle depth on the critical path — the 5-10 rules where a violation means real damage. They never miss.
Skills handle complexity — the multi-step workflows that are too procedural for rules and too important to leave to ad-hoc reasoning.
Together, they give you a system where AI agents work within guardrails — reliable enough for production, flexible enough for daily work, transparent enough to debug when something goes wrong.
If you're just getting started: write a 50-line CLAUDE.md today. Add one hook that blocks your most dangerous operation. Create one skill for your most common workflow. That foundation will serve you better than a 500-line CLAUDE.md ever could.
Next up: Part 3 crosses the boundary from Claude's internal brain to its external capabilities — MCP servers, connectors, and Claude Apps. And Part 4 composes all of these layers into a real production walkthrough.
This is Part 2 of a four-part series. Start with Part 1: The Mental Model if you haven't read it yet. For related deep dives, see Build Your Own AI Agent Tool System and Multi-Agent Systems: Orchestration from Scratch.
Build agents with built-in guardrails
Chanl gives your AI agents tools, memory, and monitoring — with the enforcement layer to keep them on track.
Explore Chanl ToolsCo-founder
Building the platform for AI agents at Chanl — tools, testing, and observability for customer experience.
Learn Agentic AI
One lesson a week — practical techniques for building, testing, and shipping AI agents. From prompt engineering to production monitoring. Learn by doing.



