Hook, skill or CLAUDE.md line? We ran all three (and nothing)
We asked Claude Code to follow a house formatting rule four ways, 3 runs each. All four worked. The cheapest was one CLAUDE.md line, and the hook cost more than we expected.
Claude Code gives you several ways to make something happen: write it in CLAUDE.md, package it as a skill, or enforce it with a hook. We tested which one actually gets a rule followed.
The setup
A small JavaScript project where every .js file must start with a license line and an integrity hash. The hash is a sha256 of the file, so Claude can’t write it by hand; it has to run npm run format.
The task: add a new coupons.js module, add a discount() function to an existing file, and write tests for both. We ran it 3 times each with:
- Nothing: no instructions at all.
- CLAUDE.md: one line saying to run
npm run formatafter editing.jsfiles. - Skill: a
house-formatskill whose description says to use it after editing.jsfiles. - Hook: a
PostToolUsehook that runs the formatter after every Edit or Write.
The hook looked like this, in .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "npm run --silent format" }]
}
]
}
}
What happened
| Nothing | CLAUDE.md | Skill | Hook | |
|---|---|---|---|---|
| Every file correctly stamped | 3 of 3 | 3 of 3 | 3 of 3 | 3 of 3 |
| Tests pass | 3 of 3 | 3 of 3 | 3 of 3 | 3 of 3 |
| Claude used the skill | 3 of 3 | |||
| Average cost | $0.110 | $0.083 | $0.084 | $0.101 |
| Average turns | 14 | 10.3 | 12.7 | 14 |
Claude Code 2.1.296, Sonnet 5.5, 3 runs per column. Every run, check and diff: run data.
What we learned
Claude found the formatter on its own. With no instructions, it read package.json, saw a format script, and ran it every time. A well-named script is already a kind of documentation.
One clear line was the cheapest. The CLAUDE.md line saved about 4 turns and a quarter of the cost compared with nothing, because Claude didn’t have to work out the rule. The skill was nearly as good, and Claude picked it up in every run. One caveat: our test setup blocks some shell commands, and the Nothing and Hook runs hit about 4 blocked commands each against 1 for the others, which adds turns. With 3 runs each, treat the cost ranking as a hint.
The hook didn’t save money here. It stamped every file without Claude doing anything, but Claude still read the hook config in all 3 runs and tried to check the result itself. The hook runs cost about as much as running with nothing.
Watch what your hook matches. In 11 of 12 runs, Claude’s first file write was a shell command (cat > file), not the Write tool. Our test setup blocked most of those, so Claude fell back to Write and the hook fired. In a setup that allows them, a hook matched only on Edit|Write would not run for that file. We didn’t test that case, so treat it as a likely gap, not a measured one.
The recipe
- Use a hook when it must never be skipped. It runs no matter what Claude decides. Make the hook cover the whole repo, or also match
Bash, because Claude sometimes writes files with shell commands. - Use a CLAUDE.md line for a simple, always-relevant rule. It was the cheapest option we tested.
- Use a skill for a procedure that only matters sometimes. Only its name and description sit in context until it’s needed, and Claude used ours every time. Skills live in
.claude/skills/<name>/SKILL.md, or~/.claude/skills/for all your projects. - Use a subagent for big read-throughs. It works in its own context and hands back a summary, so the file dumps never land in your main session. We haven’t measured this one yet.
Custom slash commands are now the simple form of a skill: .claude/commands/review.md and .claude/skills/review/SKILL.md both give you /review. If you only want a skill to run when you type it, add disable-model-invocation: true to its frontmatter.