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:

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:

  1. Startup: Claude loads only skill names and descriptions into context
  2. Triggered: When relevant, Claude reads the full SKILL.md file
  3. 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:

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