Part 7: Skills & Hooks — Enforcement Over Suggestion
This section was added based on community feedback. Special thanks to u/headset38 and u/tulensrma for pointing out that Claude doesn't always follow CLAUDE.md rules rigorously.
Why CLAUDE.md Rules Can Fail
Research on prompt-based guardrails explains:
"Prompts are interpreted at runtime by an LLM that can be convinced otherwise. You need something deterministic."
Common failure modes:
- Context window pressure: Long conversations can push rules out of active attention
- Conflicting instructions: Other context may override your rules
- Copy-paste propagation: Even if Claude won't edit
.env, it might copy secrets to another file
One community member noted their PreToolUse hook catches Claude attempting to access .env files "a few times per week" — despite explicit CLAUDE.md rules saying not to.
The Critical Difference
| Mechanism | Type | Reliability |
|---|---|---|
| CLAUDE.md rules | Suggestion | Good, but can be overridden |
| Hooks | Enforcement | Deterministic — always runs |
| settings.json deny list | Enforcement | Good |
| .gitignore | Last resort | Only prevents commits |
PreToolUse hook blocking .env edits:
→ Always runs
→ Returns exit code 2
→ Operation blocked. Period.
CLAUDE.md saying "don't edit .env":
→ Parsed by LLM
→ Weighed against other context
→ Maybe followed
Hooks: Deterministic Control
Hooks are shell commands that execute at specific lifecycle points. They're not suggestions — they're code that runs every time.
Hook Events
| Event | When It Fires | Use Case |
|---|---|---|
PreToolUse |
Before any tool executes | Block dangerous operations |
PostToolUse |
After tool completes | Run linters, formatters, tests |
Stop |
When Claude finishes responding | End-of-turn quality gates |
UserPromptSubmit |
When user submits prompt | Validate/enhance prompts |
SessionStart |
New session begins | Load context, initialize |
Notification |
Claude sends alerts | Desktop notifications |
Example: Block Secrets Access
Add to ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Edit|Write",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/block-secrets.py"
}
]
}
]
}
}
The hook script (~/.claude/hooks/block-secrets.py):
#!/usr/bin/env python3
"""
PreToolUse hook to block access to sensitive files.
Exit code 2 = block operation and feed stderr to Claude.
"""
import json
import sys
from pathlib import Path
SENSITIVE_PATTERNS = {
'.env', '.env.local', '.env.production',
'secrets.json', 'secrets.yaml',
'id_rsa', 'id_ed25519', '.npmrc', '.pypirc'
}
def main():
try:
data = json.load(sys.stdin)
tool_input = data.get('tool_input', {})
file_path = tool_input.get('file_path') or tool_input.get('path') or ''
if not file_path:
sys.exit(0)
path = Path(file_path)
if path.name in SENSITIVE_PATTERNS or '.env' in str(path):
print(f"BLOCKED: Access to '{path.name}' denied.", file=sys.stderr)
print("Use environment variables instead.", file=sys.stderr)
sys.exit(2) # Exit 2 = block and feed stderr to Claude
sys.exit(0)
except Exception:
sys.exit(0) # Fail open
if __name__ == '__main__':
main()
Hook Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success, allow operation |
| 1 | Error (shown to user only) |
| 2 | Block operation, feed stderr to Claude |
Hook Input Format
Hooks receive JSON via stdin containing context about the current operation.
PreToolUse / PostToolUse:
{
"hook_type": "PreToolUse",
"tool_name": "Read",
"tool_input": {
"file_path": "/path/to/file.js"
},
"session_id": "abc123-def456"
}
Stop (end of turn):
{
"hook_type": "Stop",
"stop_reason": "end_turn",
"transcript_path": "/tmp/claude/transcript.json"
}
Skills: Packaged Expertise
Skills are markdown files that teach Claude how to do something specific — like a training manual it can reference on demand.
From Anthropic's engineering blog:
"Building a skill for an agent is like putting together an onboarding guide for a new hire."
How Skills Work
Progressive disclosure is the key principle:
- Startup: Claude loads only skill names and descriptions into context
- Triggered: When relevant, Claude reads the full
SKILL.mdfile - As needed: Additional resources load only when referenced
This makes skills extremely token efficient. A 500-line skill costs zero tokens until triggered.
Rule of thumb: If instructions apply to <20% of your conversations, make it a skill instead of adding it to CLAUDE.md.
Skill Structure
.claude/skills/
└── commit-messages/
├── SKILL.md ← Required: instructions + frontmatter
├── templates.md ← Optional: reference material
└── validate.py ← Optional: executable scripts
SKILL.md (required):
---
name: commit-messages
description: Generate clear commit messages from git diffs. Use when writing commit messages or reviewing staged changes.
---
# Commit Message Skill
When generating commit messages:
1. Run `git diff --staged` to see changes
2. Use conventional commit format: `type(scope): description`
3. Keep subject line under 72 characters
## Types
- feat: New feature
- fix: Bug fix
- docs: Documentation
- refactor: Code restructuring
When to Use Skills vs Other Options
| Need | Solution |
|---|---|
| Project-specific instructions | Project CLAUDE.md |
| Reusable workflow across projects | Skill |
| External tool integration | MCP Server |
| Deterministic enforcement | Hook |
| One-off automation | Slash Command |
Combining Hooks and Skills
The most robust setups use both:
- A
secrets-handlingskill teaches Claude how to work with secrets properly - A
PreToolUsehook enforces that Claude can never actually read.envfiles
Updated Defense in Depth
| Layer | Mechanism | Type |
|---|---|---|
| 1 | CLAUDE.md behavioral rules | Suggestion |
| 2 | PreToolUse hooks | Enforcement |
| 3 | settings.json deny list | Enforcement |
| 4 | .gitignore | Prevention |
| 5 | Skills with security checklists | Guidance |