TL;DR and verdict
Four jobs you keep typing into Claude Code each belong to a different file. A release checklist is a skill. A rule that must never break is a PreToolUse hook. An audit that reads a lot of files is a subagent. Data Claude cannot see is an MCP server.
We ran all four on one real repo, an invoicing API with 34 tests, and made the wrong pick first each time. The wrong picks failed on screen:
- The checklist in CLAUDE.md added a
Memory files: 210 tokensline to /context, on every turn. - The first hook ran, printed its warning, and the write went through (unitPrice 8000 to 12000).
- The audit run inline filled the thread: Messages at 19.3k tokens, against 7.2k with a named subagent.
- Without a docs server, Claude answered "I couldn't find that setting... I won't guess one."
Verdict: pick the kind from the docs table, then make the file answer, not just exist. A hook that only logs is advisory, like the CLAUDE.md line it replaced. Of the four, only the hook never asks.
Copy the files below, commit them, and start with the one you retype most.
Sourced synthesis
What your session loads before your first word. Our baseline /context read 26.2k/1m tokens, with no memory line and no MCP line. Every job you add either puts a line in that table or does not. Read it first, then measure against it.
A CLAUDE.md rule is a request, a hook is enforcement. The docs say an instruction like "never edit .env" in CLAUDE.md or a skill is a request, and a PreToolUse hook that blocks the edit is enforcement s1. The Memory page calls CLAUDE.md context, not enforcement s8. A thread on Hacker News reported the same thing from the hook side: a hook that exits 0 blocks nothing s10.
The trigger table tells you which file. Third paste of a procedure means a skill. Output that floods the chat means a subagent. A tab Claude cannot see means an MCP server. Something that must happen every time means a hook s1.
Job 1, the release checklist, is a skill.
- A procedure that grew inside CLAUDE.md belongs in a skill, and its body loads only when used s3.
- In our run the checklist pasted into CLAUDE.md added a
Memory files: 210 tokensline. As a skill withdisable-model-invocation: true, /context went back to baseline. /releaseran six steps (34 tests, 0.4.0 to 0.4.1, tag v0.4.1). It also caught thatgit commit -amskips untracked files, so it staged the new changelog first.
Job 2, the rule that must hold, is a hook, and the guarantee is the answer it returns.
- Exit 0 with no output is no decision. Exit 2 blocks. The deny JSON blocks too s2.
- Our first hook logged, exited 0 and the write went through (unitPrice 8000 to 12000). Claude's reply never mentioned the hook.
- The same hook returning the deny JSON refused the edit with
PreToolUse:Edit hook error: Stored data is never edited by hand: write a migration in src/store/migrations.ts (README § Conventions).Claude then asked: override, or write the migration. - The docs publish a PreToolUse recipe for protected files s23.
- The limit: this hook watches Edit and Write. Cover the shell too, or keep the shell off your allow list.
onFailure: "block"makes a failed hook block instead of failing open. It arrived in v2.1.295 s2 s15.
Job 3, the audit that floods, is a subagent.
- A subagent starts blind and returns a summary s4.
- Inline, Messages read 19.3k. With the named subagent it read 7.2k.
- The agent file alone delegated 0 of 2 runs. Naming it in the prompt delegated. File makes it exist, your prompt makes Claude use it.
- Pin
model:in the file. The bill did not move: the reading still happens, just not in your window.
Job 4, data Claude cannot reach, is an MCP server.
claude mcp add --scope projectwrites.mcp.json, and tool search loads tool names only s5.- A free, no-login HTTP server exists for Claude Code's own docs, with
search_claude_code_docsandquery_docs_filesystem_claude_code_docss24. - Without it, Claude answered "I couldn't find that setting... I won't guess one." With it, Claude answered
"onFailure": "block", added in v2.1.295, while our install was 2.1.294. - A docs server for a library the model already knows changed nothing for us. Connect what the model cannot have.
Field reports. A Reddit thread reported Claude ignoring a "MUST use agent" line in CLAUDE.md s9. We cite it as a report, not a measurement. Anthropic's engineering posts cover the same split s6 s7.
Code
Four files from the run, plus two helpers. Paths are where each one lives in your repo.
1. The release skill, at .claude/skills/release/SKILL.md. Type /release. disable-model-invocation: true keeps it yours to start, which is what you want for something that commits and tags.
SKILL.md
---
name: release
description: Cut a patch release of ledgerly (typecheck, tests, version bump, changelog line, commit, tag).
disable-model-invocation: true
---
Run these steps in order and stop at the first failure:
1. `pnpm typecheck` must exit 0.
2. `pnpm test` must pass.
3. Bump the patch version in `package.json` (e.g. 0.4.0 -> 0.4.1).
4. Add one line at the top of `CHANGELOG.md` (create it if missing): `## vX.Y.Z - YYYY-MM-DD` followed by a one-line summary of the commits since the last tag.
5. `git commit -am "release vX.Y.Z"`
6. `git tag vX.Y.Z`. Never push.
2. The protect-data hook, in .claude/settings.json. It returns the deny decision with a reason Claude reads. It needs jq on your PATH and fits inline, so the hook needs no script file of its own. Change the data/.*\.json$ pattern and the reason to your rule.
settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -c 'if (.tool_input.file_path // \"\") | test(\"data/.*\\\\.json$\") then {hookSpecificOutput: {hookEventName: \"PreToolUse\", permissionDecision: \"deny\", permissionDecisionReason: \"Stored data is never edited by hand: write a migration in src/store/migrations.ts (README § Conventions).\"}} else empty end'"
}
]
}
]
}
}
3. The conventions auditor, at .claude/agents/conventions-auditor.md. Tools are limited to read-only ones and the model is pinned. Edit the conventions list to match your README.
conventions-auditor.md
---
name: conventions-auditor
description: Use when asked to audit the code against the README conventions
model: sonnet
tools: Read, Grep, Glob
---
You audit ledgerly's source against README § Conventions:
- Money is always an integer number of cents. Never floats.
- Domain functions return new objects; they never mutate their input.
- Validation failures throw `ValidationError` (400), missing records `NotFoundError` (404), illegal state transitions `ConflictError` (409).
- A change of stored shape bumps `schemaVersion` and adds a migration in `src/store/migrations.ts`.
Read every file under `src/`. Return only the findings: one line per violation as `file:line - convention - what is wrong`, then borderline cases the same way, then one line with the count of files read. No file contents, no summary of clean files.
conventions-auditor.md on GitHub
4. The MCP config, saved as .mcp.json at the repo root. It is what claude mcp add --transport http claude-docs --scope project https://code.claude.com/docs/mcp writes. Claude Code lists a project server as pending until you approve it in your next session.
mcp.json
{
"mcpServers": {
"claude-docs": {
"type": "http",
"url": "https://code.claude.com/docs/mcp"
}
}
}
Helpers. ctx.sh runs /context in a headless session and prints the token lines, so you can compare before and after. mcp-probe.sh sends initialize and tools/list to an HTTP MCP url, to check a server answers before you add it.
ctx.sh
#!/usr/bin/env bash
# Usage: ctx.sh <step> -> runs "/context" through run-session.sh, writes context-<step>.txt (honours STRICT_MCP / EXTRA_ALLOW)
C="$(cd "$(dirname "$0")" && pwd)"
"$C/run-session.sh" "$C/out/context-$1.json" "/context" >/dev/null
python3 -c "import json,sys;d=json.load(open(sys.argv[1]));print(d['result'])" "$C/out/context-$1.json" > "$C/context-$1.txt"
grep -E '^\*\*Tokens|^\| (System prompt|System tools|MCP tools|Memory files|Skills|Custom agents|Messages) ' "$C/context-$1.txt"
mcp-probe.sh
#!/usr/bin/env bash
# Usage: mcp-probe.sh <url> [tools/call json params] -> initialize + notifications/initialized + tools/list (+ optional tools/call) over streamable HTTP
U="$1"; H=$(mktemp)
curl -s -D "$H" -m 30 -X POST "$U" -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
echo; SID=$(grep -i '^mcp-session-id:' "$H" | tr -d '\r' | cut -d' ' -f2); echo "session-id: ${SID:-none}"
HS=(-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream'); [ -n "$SID" ] && HS+=(-H "Mcp-Session-Id: $SID")
curl -s -m 30 -X POST "$U" "${HS[@]}" -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' >/dev/null
curl -s -m 30 -X POST "$U" "${HS[@]}" -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'; echo
[ -n "${2:-}" ] && { curl -s -m 90 -X POST "$U" "${HS[@]}" -d "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":$2}"; echo; }
rm -f "$H"
Do this Monday
- Open a fresh session and run /context. Write down the total. That is your baseline.
- Pick the checklist you paste most. Move it to
.claude/skills/<name>/SKILL.md, adddisable-model-invocation: trueif it commits or tags, and run it with/<name>. - Take the one rule that must never break and put it in a PreToolUse hook that returns
permissionDecision: "deny"or exits 2. Ask Claude to break the rule, and check the edit is refused. - If your hook only logs, change it. A hook that exits 0 blocks nothing.
- Move one task that reads many files into
.claude/agents/<name>.mdwith a pinnedmodel:. Name it in your prompt until Claude picks it by itself. - Check one fact Claude guessed for you recently. If it is newer than the model, add a docs server with
claude mcp add --transport http <name> --scope project <url>and commit.mcp.json. - Check your Claude Code version before you rely on a setting.
onFailure: "block"needs 2.1.295. - Run
git add -A && git statusand commit the new files. Our run showed exactly four.
Go further
The picking card. One page. These are the docs rows s1 with our four jobs written on them. The hero of this pack is the four files above plus this card.
| Kind | What it does | Use it when | Our job | Our file |
|---|---|---|---|---|
| CLAUDE.md | Persistent context loaded every conversation | A convention Claude gets wrong twice | One line, not a procedure | none |
| Skill | Instructions and workflows Claude can use | You paste the same playbook for the third time | The release checklist | .claude/skills/release/SKILL.md |
| Hook | A script triggered by events | You want it to happen every time, without asking | Never edit stored data by hand | .claude/settings.json |
| Subagent | Isolated context that returns summarized results | A side task floods your conversation | The conventions audit | .claude/agents/conventions-auditor.md |
| MCP | Connect to external services | You keep copying data from a tab Claude cannot see | The Claude Code docs | .mcp.json |
| Plugin | Bundles the four into one installable unit | A second repository needs the same setup | Not used | none |
The card decides the kind, not the quality. A hook that only logs, a subagent you never name and a server for docs the model knows each sat in the right kind and failed. Pick the kind, then make the file answer.
When not to reach for these.
- A convention Claude keeps getting wrong stays in CLAUDE.md as a line, not as a procedure.
- A job you steer back and forth, with your history in view, stays in the main thread, not in a subagent.
- A docs server for a library the model already knows costs context and changes nothing.
pnpm testis a CLI you already have. It is not an MCP job.
Plugins. A plugin is the box that ships the four together. Reach for one when another repo needs the same setup.
Skill plus MCP. The docs pair them: the server gives the connection, and the skill teaches Claude how to use it well.
Skill with context: fork. A skill can run in a forked subagent, so its reading stays out of your window. Test it on one job before you rely on it.
Hook types beyond command. Command is the one we used. The hooks reference lists other handler types, including http and prompt hooks. The same rule holds: what the hook returns decides, and a silent hook decides nothing.
MCP versus CLI. There is an open debate about MCP servers eating context, with a post arguing for the CLI first s11 and another on the context cost of MCP servers s12. Our rule: if a CLI already does the job, use it. Use MCP for what has none. Tool search now loads names only, so measure with /context on your own setup before you decide s5.
Keep skills short and verified. One short skill per job, checked against a real run. See the SkillsBench paper s13 and a practitioner view of why skills cost little until used s14.
FAQ
Why did my hook run but the edit still went through? A hook decides only through what its command returns. Exit 0 with no output is no decision s2. Return the deny JSON, or exit 2.
Why does Claude read the files itself when I have a subagent? In our run the agent file alone delegated 0 of 2 runs. Name it in the prompt, for example "use the conventions auditor subagent".
Does the hook cover everything Claude can do? No. Ours matches Edit and Write. Claude offered to make the change through the shell in a later run, so cover the shell or keep it off your allow list.
Should I add an MCP server for every library I use? No. If the model already knows the library, a server changes nothing and costs context. Add one for data newer than the model or off your disk, and check your installed version against the one that added the feature.
Can I commit all of this?
Yes. The skill, settings file, agent file and .mcp.json all live in the repo, so anyone who clones it gets them. Approve the project MCP server once in your next session.
AIDive