Choose CLAUDE.md, Hooks, or Skills for AI-Assisted Development
This guide is for engineers who already use coding assistants such as Claude Code. It explains when to use CLAUDE.md, Hooks, or Skills, then shows how to package a repeatable release-notes workflow as a Skill with metadata, scripts, and acceptance checks.
Who this is for and what you need first
This article is for engineers who already use AI coding assistants such as Claude Code in day-to-day development. Before you follow it, you need a Git repository, the ability to run shell scripts in a terminal, and a clear understanding of your project’s test commands and code conventions.
The directory layout, file names, and fields here match Claude Code versions that support Skills. Field names can change between versions, so check them against the official documentation for the version you use. These settings directly change how the AI behaves. Try them first on a personal branch or in a test repository, and merge them into the team’s main line only after they work as expected.
Step 1: When to use CLAUDE.md, Hooks, or Skills
The three mechanisms solve different problems. Choose by two criteria: when the instruction takes effect, and whether the AI can skip it.
Start with this decision tree:
[I want the AI to remember or do something]│▼Is this needed in every session?│├─ Yes → CLAUDE.md (loaded automatically when a session starts)│└─ No → Must it run whenever a specific event happens?│├─ Yes → Hooks (run by the client, not by the AI’s judgment)│└─ No → Is it a fixed multi-step workflow?│├─ Yes → Skills (loaded when the task matches)│└─ No → Is it low-frequency reference material?│├─ Yes → Put it in project docs and note the path in CLAUDE.md│└─ No → Do not configure it yet; package it after it repeats
After the decision tree, compare the three options:
| Dimension | CLAUDE.md | Hooks | Skills |
|---|---|---|---|
| When it applies | Loaded when a new session starts | When a specified event occurs (for example, before or after a tool call, or when a session ends) | When the task matches the Skill description |
| How strict it is | Advisory; the AI may ignore it or misread it | Deterministic; a script can block later actions by returning a specific exit code | Depends on the model following the steps; the workflow itself is not enforced |
| Maintenance cost | Low, but harder to maintain as it grows | Medium; scripts need review and testing | Medium to high; metadata, scripts, and examples all need upkeep |
| Typical use | Project background, stack, directory conventions, naming rules | Pre-commit checks, blocking edits to protected files | Multi-step tasks such as release notes, dependency upgrades, and troubleshooting |
Two points matter. First, CLAUDE.md is guidance, not a constraint. The AI can still drift from rules written there during a complex task, so security or compliance requirements belong in Hooks. Second, do not put every piece of low-frequency knowledge into CLAUDE.md. It consumes context in every session. Keep infrequent material in separate documents and point to those paths from CLAUDE.md.
Step 2: Write a Skill that actually runs
A Skill is useful when it freezes a repeated multi-step task. The example below generates release notes from commits between two Git tags, including the full directory and file contents.
Directory layout:
.claude/skills/release-notes/├── SKILL.md # Metadata and step instructions├── scripts/│ ├── collect_commits.sh # Collect commit history│ └── validate_output.py # Check output format└── examples/├── input.md # Sample input└── expected_output.md # Expected output for acceptance
SKILL.md is below. The metadata at the top of the file decides whether the AI can find and load the Skill in the right situation, so the description must state when to use it:
---name: release-notesdescription: Generate release notes from commits between two Git tags. Use when the user asks for release notes, a changelog, or an update log.---# Generate release notes## Inputs- from_tag: start tag, required, for example v1.2.0- to_tag: end tag, required, for example v1.3.0If either parameter is missing, ask the user. Do not guess.## Steps1. From the project root, run `bash .claude/skills/release-notes/scripts/collect_commits.sh <from_tag> <to_tag>` to get the commit list.2. Group commits by message prefix: feat under “New features,” fix under “Bug fixes,” refactor under “Internal refactors,” and everything else under “Other.”3. Keep the commit hash and a one-sentence description for each entry.4. Run `python3 .claude/skills/release-notes/scripts/validate_output.py <output file>` to check the format.## Success criteria- The number of generated entries matches the number of commits from the collection script.- Every entry includes a commit hash.- The validation script returns 0.## Failure handling- Step 1 fails because a tag does not exist: stop and report the missing tag name.- Step 2 finds commits that cannot be classified: put them under “Other” and list those commit hashes in the report.- Step 4 fails: fix the format and retry once. If it still fails, keep the draft, do not overwrite the official file, and report the first error line from the validator.
The commit-collection script is below. Tag names come from user input, so the script quotes every variable and checks that each tag exists before use, so unexpected input is not concatenated into a command:
#!/usr/bin/env bashset -euo pipefailfrom_tag="${1:?missing start tag}"to_tag="${2:?missing end tag}"for tag in "$from_tag" "$to_tag"; doif ! git rev-parse --verify --quiet
