Extending Claude Code: Hooks (Deterministic) vs. CLAUDE.md (Advisory), Skills, Subagents, Plugins, Wiring Up an MCP Server, CLI Tools First
CLAUDE.md is only advisory; hooks are the real gate. Learn to make "must happen every time" rules deterministic with hooks, package reusable workflows as a skill, delegate research to a subagent, then wire up MCP and CLI tools.
Today's Goals
- Explain why hooks are more reliable than rules in CLAUDE.md, and write one hook that blocks a dangerous action
- Write a SKILL.md, and explain what each of skill, CLAUDE.md, and subagent is for
- Wire an MCP server into Claude Code, and explain the case for preferring CLI tools
Yesterday you wrote the teammate an onboarding handbook. Today we answer what a handbook cannot: which rules must be enforced without exception, which workflows deserve a written procedure, and which work should be handed off. Once you have read this and finished the lab, scroll back up and tick off the three goals.
Plain-Language Walkthrough
Advice versus a gate: CLAUDE.md says "please do this," hooks guarantee "this always happens"
The handbook says badge in at the server room door. Most colleagues will; someone in a hurry will slip in the back. What actually guarantees the badging is not the handbook but the gate — no badge, no door, regardless of anyone's diligence.
CLAUDE.md is the handbook and hooks are the gate. That is not a figure of speech but a consequence of where each sits in the system: the content of CLAUDE.md goes into the model's context as text, and the model reads it and then decides — it is advisory, with a high compliance rate but not a perfect one, and compliance drops as the file grows, as we saw yesterday. A hook, by contrast, is a script that the Claude Code program executes unconditionally at a specific moment: before a tool call, after one, before a turn ends, at session start. Whether it runs and whether it blocks is decided by its exit code, and nothing the model says changes that. It is deterministic.
So the criterion is one sentence: anything that must happen every time, with not a single exception, becomes a hook; anything that usually ought to happen goes in CLAUDE.md. The official best practices page gives this its own section, plus the reverse advice: if Claude already follows a CLAUDE.md rule by default, delete it; if a rule must be followed 100% of the time, convert it into a hook. Shave both ends and CLAUDE.md gets short.
A typical must-happen list: auto-format after an edit; block anything that touches migrations/; do not let a turn end while tests fail; record every command in an audit log. A typical usually-ought-to list: prefer named exports; write commit messages in a particular style; ask before guessing at an ambiguous requirement. The first list goes to the gate, the second stays in the handbook.
Writing your first hook: event, matcher, script, exit code
A hook is made of four things: when it fires (the event), which tools it fires for (the matcher), what runs (the command), and how the result is expressed (an exit code or JSON). The configuration lives in the project's .claude/settings.json (travels with git, shared by the team) or ~/.claude/settings.json (yours alone), and /hooks lets you browse what is currently in effect.
The commonly used events are these: PreToolUse (before a tool call, and it can block), PostToolUse (after a call, so it can follow up), Stop (when the model is about to end a turn, and it can refuse to let it), UserPromptSubmit (when you submit a prompt), and SessionStart. In the example task we want to block writes to the migrations directory, so PreToolUse; the matcher takes tool names, and Edit|Write matches both editing tools:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node ${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-migrations.mjs"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/require-green.sh",
"timeout": 120
}
]
}
]
}
}The script reads one JSON object from standard input: it holds hook_event_name, tool_name, tool_input (for Edit, fields such as file_path), cwd, and session_id. It expresses its decision through the exit code: 0 allows (and if stdout carried a JSON object, Claude Code parses the structured decision in it); 2 blocks, with whatever went to stderr handed back to the model as the reason it was blocked so it can take a different route; any other non-zero code means the script itself failed but nothing is blocked. The guard script in both languages:
// PreToolUse hook: block any write under migrations/.
// Reads JSON from stdin; exit code 2 = block, and stderr goes back to the model as the reason.
import { readFileSync } from 'node:fs'
const input = JSON.parse(readFileSync(0, 'utf8')) // 0 = stdin
const filePath = input.tool_input?.file_path ?? ''
if (/(^|\/)migrations\//.test(filePath)) {
process.stderr.write(
`Refused: ${filePath} is under migrations/. Migration files are generated by pnpm db:generate and must not be hand-edited.`
)
process.exit(2) // 2 = block this tool call
}
process.exit(0) // 0 = allow; continue through the normal permission flow# PreToolUse hook: block any write under migrations/.
# Reads JSON from stdin; exit code 2 = block, and stderr goes back to the model as the reason.
import json
import re
import sys
payload = json.load(sys.stdin)
file_path = payload.get("tool_input", {}).get("file_path", "")
if re.search(r"(^|/)migrations/", file_path):
print(
f"Refused: {file_path} is under migrations/. Migration files are generated by "
"pnpm db:generate and must not be hand-edited.",
file=sys.stderr,
)
sys.exit(2) # 2 = block this tool call
sys.exit(0) # 0 = allow; continue through the normal permission flowThe script behind that Stop hook is shorter still: run the tests, and on failure exit 2 with a failure summary on stderr — the model receives "tests are red, you may not stop," keeps fixing, and continues until they pass. That is the third rung of yesterday's "make the check harder." One safety valve to know about: after being blocked by a Stop hook 8 times in a row, Claude Code lets the turn end anyway, so you cannot get stuck in an infinite loop.
Skills: package a repeated workflow as a procedure that loads on demand
Some work you brief the teammate on every week: "the flow for fixing an issue is read the issue, search the code, write a test, change it, run lint, open a PR." Put it in CLAUDE.md? You could, but that loads on every session while you only fix two issues a week — the rest of the time those few dozen lines are occupying the window for nothing. Better to write it as a printed procedure the teammate can pull off the shelf when it is needed.
That is a skill: .claude/skills/<name>/SKILL.md, a Markdown file with frontmatter. Its defining property is progressive loading — at session start only the single description line from the frontmatter stays resident, and the body is read in when Claude judges the current task relevant (or when you type /name). So the body can be long, with steps and examples, at almost no everyday cost. The skill for the example task:
---
name: add-validation
description: Add zod input validation to an Express route and write the matching vitest cases. Use when the user mentions adding validation, adding tests, or validating a request body.
argument-hint: [route path, such as POST /todos]
---
Add input validation and tests for $ARGUMENTS in the following order, finishing each
step before moving to the next:
1. Read the route file and the existing tests, and list how each field of the request body is handled today
2. Write the failing tests first: one each for empty value, over-long value, wrong type, and extra field
3. Define the schema with zod (always .strict()); on failure return 400 with { error, issues }
4. Run only this one test file: pnpm vitest run test/<file>
5. Once green, paste the test output in your reply, then stop and wait for confirmationThe description is the trigger, and there is craft to it: say what it does and how the user is likely to phrase the request, in a sentence or two. Too broad and it fires spuriously; too narrow and it never fires. $ARGUMENTS catches whatever you pass after /add-validation POST /todos. Two optional fields worth remembering: disable-model-invocation: true restricts it to manual /name invocation, which suits workflows with side effects such as deploying or sending messages; and allowed-tools can pre-authorize a few commands for this skill.
The division of labor among three things is now clear: CLAUDE.md holds short facts that are true every time, and a skill holds a multi-step workflow used only occasionally, while the subagent of the next section holds work that needs its own context. To decide where a piece of content belongs, ask two questions: is it useful on every session, and is it one rule or a set of steps?
Subagents: a colleague you send off to research, who comes back with the conclusion only
You ask the teammate to find out how your authentication handles token refresh. They go through thirty files — and if all thirty end up spread across the desk you share, the desk is full and the implementation you actually wanted has nowhere to go. Better to have them read in the office next door and come back with one conclusion.
A subagent is that office next door: a Claude with its own separate context window that takes a task, reads everything it needs, and returns only a summary to the main session. D2 and D3 kept insisting that context is the scarcest resource, and a subagent is the most direct way to protect it — isolating work that reads a lot and keeps little. Claude Code ships a few: Explore is read-only and built for finding files and understanding code; Plan does research in plan mode; general-purpose does anything. You can define your own in .claude/agents/<name>.md:
---
name: security-reviewer
description: Review code for security problems: injection, authorization flaws, leaked secrets. Use when the user asks for a security review.
tools: Read, Grep, Glob, Bash
model: sonnet
---
You are a senior security engineer. Review only; never modify. For every problem, give
the file and line number, an explanation of the risk, and a suggested fix.tools limits what it may use (a reviewer should not have Edit), and model lets you give it a cheaper or a stronger model. Usage is direct: "use a subagent to investigate the token refresh logic in the auth module," or "have security-reviewer review this diff." It cannot see your main session's history — only the task you sent it on plus CLAUDE.md. That is both a limitation and a virtue: a reviewer uncontaminated by the memory of having just written that code finds flaws more readily. D5's adversarial review is built on exactly this property.
When should you not use one? Work that needs several rounds of back-and-forth discussion, work whose phases must share a great deal of context, and work that a single sentence would finish. Sending a task out costs one full briefing plus one summary, so it does not pay for something small.
Plugins and MCP: bringing in capabilities other people already built
Everything so far has been do-it-yourself: write the handbook, write the gate, write the procedure, write the subagent. But plenty of capabilities already exist, and wiring one in beats building it. Two routes.
A plugin is a packaged bundle of skills, hooks, subagents, and MCP configuration, installed with one command. Open the marketplace with /plugin and browse: a project in a strongly typed language, for example, can install a code intelligence plugin that gives Claude precise symbol navigation and automatic error reporting after a change.
An MCP (Model Context Protocol) server turns an external system into tools Claude can call: an issue tracker, a database, a monitoring platform, a design tool. Wiring one up takes:
# a remote server (HTTP)
claude mcp add --transport http notion https://mcp.notion.com/mcp
# a local server (stdio, one executable command)
claude mcp add my-db -- npx -y @some/postgres-mcp postgres://localhost:5432/appOnce connected, Claude has a new group of tools and can read the requirement from issue #123 and implement it, or check the recent slow queries on a table. MCP is a seven-day course of its own (mcp-7days, coming soon); today it is enough to be able to wire one up and use it.
One caution: every MCP server you connect brings all of its tool definitions into the context, so ten servers can mean tens of thousands of tokens of fixed overhead. Connect what you will genuinely use, and remove what you are done with.
CLI tools first: why gh beats calling the GitHub API
The last point looks the most low-tech and yet has its own section in the official best practices: if a command-line tool exists, do not have Claude call the API directly.
The reason is context again. Having Claude create an issue through GitHub's REST API means constructing the request, handling authentication, and parsing a large JSON response — and the input and output of every step enters the window. Switch to gh issue create --title ... --body ... and it is one command, one line of output, with login and pagination already handled by gh. The same holds for aws, gcloud, sentry-cli, docker, and kubectl: a CLI is the highest-information-density interface humans have spent years polishing, and it is what Claude knows best.
It can even teach itself a CLI it has not seen: "get to know the foo tool with foo --help, then use it to do A, B, and C" — one --help output is usually enough. So before you write your own MCP server, ask whether the system already has a CLI. If it does, install it and move on.
Taking today's six sections together, the ways to extend Claude Code line up by how deterministic they are: a hook is hardest (it always executes), CLI and MCP are capabilities (available once connected), a skill is an on-demand workflow, a subagent is an isolated context, and CLAUDE.md is softest (advice). The order in which you equip the teammate is the same: install the gates you cannot do without, and only then talk about making it more capable.
Source Reading
Hands-On Lab
The lab's starter and solution directories are themselves a demo project of a few dozen lines (Express + zod + vitest), with hooks and the skill under .claude/. The hook scripts and SKILL.md in starter/ have blanks; solution/ is complete. Both sides ship an event simulator — it needs no Claude Code, feeding fake event JSON straight into the hook scripts and printing exit codes so you can iterate quickly while writing them. The first three steps need no Claude Code; the last two do.
- Run pnpm install in the starter directory, then pnpm start, and watch the simulator report all three events as allowed — because the hooks are still empty, which is what you are filling in.
- Finish exercise 1: read the stdin JSON in guard-migrations.mjs, and when the path matches migrations/, write the reason to stderr and exit 2. Rerun the simulator and confirm the first event becomes blocked.
- Finish exercise 2: in require-green.sh, run the demo project's single test file, and on failure write the last few lines of output to stderr and exit 2. Break an assertion on purpose, rerun the simulator to see the effect, then fix it back.
- Finish exercise 3: complete the description and the five-step body of SKILL.md, making sure the description says how a user would phrase the request.
- Open the solution directory in real Claude Code, check the configuration with /hooks, then ask it to rename a table in migrations/0001_init.sql and watch it get blocked; finally run /add-validation POST /todos through the whole flow.
Interview Questions
Today's 3 questions are in the question bank below, weighted toward the reliability gap between hooks and CLAUDE.md, the progressive loading of a skill, and the context isolation of a subagent. Expand each one and read the analysis before the key points — practicing the derivation beats memorizing bullets. The high-frequency labels for the domestic and overseas markets are there so you can pick by target market.
Checklist and Tomorrow
- Explain why hooks are more reliable than rules in CLAUDE.md, and write one hook that blocks a dangerous action
- Write a SKILL.md, and explain what each of skill, CLAUDE.md, and subagent is for
- Wire an MCP server into Claude Code, and explain the case for preferring CLI tools
- Recite what each of the three hook exit codes means, and the 8-strike safety valve on Stop hooks
- All 5 acceptance criteria of the lab pass
- Answer at least 2 of the 3 interview questions without looking at the key points
Tomorrow (D5) is the last day, and we turn Claude Code from a terminal sidekick into one stage of a pipeline: run it headless in CI with claude -p, parse the JSON result to judge success or failure, open parallel sessions with worktrees, run an adversarial review across Writer / Reviewer sessions, and finish with a 20-line minimal agent built on the Agent SDK. The gates you installed today only show their real value when nobody is watching — with no human present, a hook is the only rule still working.
Interview questions
Why are hooks more reliable than rules in CLAUDE.md? What belongs in each? Give one rule you would move from CLAUDE.md to a hook.为什么 hooks 比 CLAUDE.md 里的规则更可靠?各适合放什么?举一个你会从 CLAUDE.md 挪到 hook 的例子。
Common in ChinaCommon overseasBasic#hooks#claude-mdHow to reason about it · think before answering
- This tests the systemic position of advisory versus deterministic, not feature recall. 'Hooks are scripts that run automatically' is a description; explain why model adherence is not program execution.
- Breakdown: CLAUDE.md enters the model's context as text and the model decides after reading — adherence is high but not total, drops as the file grows, and can be lost after compaction. A hook is a script Claude Code itself runs unconditionally at fixed lifecycle points (PreToolUse, PostToolUse, Stop), with the exit code deciding whether to block, independent of the model's judgment.
- One-line rule: actions that allow zero exceptions become hooks; preferences that usually apply stay in CLAUDE.md. The inverse also holds — delete rules the model follows by default, convert must-always rules into hooks, and the file shrinks.
- Make the example concrete: 'run lint and tests before committing' is occasionally skipped as text; as a Stop hook, failing tests exit 2 and the model receives the summary and keeps fixing. 'Never edit migrations/' becomes a PreToolUse hook matching Edit|Write that exits 2 on a path hit.
- Follow-ups: risks? Hooks are code running on your machine — a cloned repo's hooks execute, and headless mode shows no trust dialog; a Stop hook is overridden after 8 consecutive blocks to prevent loops.
分析过程 · 先想清楚再作答
- 这题考的是「建议 vs 确定性」的系统位置,不是背功能名。答「hooks 是自动执行的脚本」只是描述,要说清为什么模型的遵守率不等于程序的执行率。
- 拆法:CLAUDE.md 的内容作为文字进入模型上下文,由模型读后决定怎么做——遵守率高但不是百分之百,文件越长越低,压缩后还可能丢失。hook 是 Claude Code 程序在固定生命周期点(PreToolUse / PostToolUse / Stop 等)无条件运行的脚本,由退出码决定拦不拦,与模型的判断无关。
- 判据一句话:一次例外都不能有的动作做成 hook;通常应该这样的偏好写进 CLAUDE.md。反向操作也成立:CLAUDE.md 里模型已经默认遵守的删掉,必须百分之百的换成 hook,文件就短了。
- 例子要具体:「提交前跑 lint 与测试」——作为文字它偶尔会被跳过;做成 Stop hook,测试不过 exit 2,模型收到失败摘要继续修,直到通过;「不许改 migrations/」做成 PreToolUse hook 匹配 Edit|Write,路径命中就 exit 2。
- 可预期的追问:hook 有没有风险?有——它是代码,跑在你机器上,clone 陌生仓库时别人的 hook 会执行,无头模式没有信任对话框;Stop hook 连续 8 次阻止后会被放行防死循环。
Key points
- CLAUDE.md is text the model reads and then decides on — high but not total adherence
- A hook is a script the program runs unconditionally at lifecycle points; the exit code decides, not the model
- Zero-exception actions become hooks; usual preferences stay in CLAUDE.md
- Examples: pre-commit tests as a Stop hook; a migrations deny as a PreToolUse hook
答题要点
- CLAUDE.md 是送进上下文的文字,由模型读后决定,遵守率高但不是百分之百
- hook 是程序在固定生命周期点无条件跑的脚本,退出码决定拦不拦,与模型判断无关
- 一次例外都不能有的做 hook;通常应该这样的写 CLAUDE.md
- 例:提交前测试改成 Stop hook;禁改 migrations 改成 PreToolUse hook
What is progressive loading for skills, why does it save context, and how should the description be written?skill 的渐进式加载是怎么回事?为什么能省上下文?description 应该怎么写?
Common in ChinaCommon overseasIntermediate#skills#contextHow to reason about it · think before answering
- This tests the on-demand loading idea and whether you have actually written a skill. The third part separates candidates: a poorly written description makes the skill dead weight.
- Mechanism: at session start only each skill's one-line description from the frontmatter is resident; the body loads when the model judges the task relevant or the user types /name. Body length therefore barely affects daily cost, so it can hold long procedures, examples, and caveats.
- Contrast with CLAUDE.md: loaded in full every session, a fixed cost; a procedure used twice a week wastes the window the rest of the time. Moving such content into skills is how CLAUDE.md keeps shrinking after pruning.
- Writing the description: state what it does plus the phrases a user would say, about a hundred words; too broad triggers on unrelated tasks, too narrow never triggers. Add disable-model-invocation: true for side-effecting workflows so only /name invokes them; $ARGUMENTS takes parameters; allowed-tools pre-approves commands.
- Follow-ups: how do you test triggering? Try several natural phrasings and check whether the body loaded. Skill versus subagent: a skill loads a manual into the current context; a subagent opens a separate context to do work; they compose.
分析过程 · 先想清楚再作答
- 这题考的是「按需加载」这个设计思想,以及你有没有真写过 skill。第三问是区分度:description 写不好,skill 就形同虚设。
- 机制:会话开始时只有每个 skill 的 frontmatter 里那一行 description 常驻上下文;当模型判断当前任务相关、或用户输入 /name 时,正文才被读进来。所以正文长短几乎不影响日常成本,可以放几十步的流程、示例、注意事项。
- 对比 CLAUDE.md:它整份每次加载,是固定成本;一周只用两次的流程放进去等于其余时间白占窗口。把这类内容挪到 skill,是「删到不能再删」之后 CLAUDE.md 还能继续变短的主要手段。
- description 的写法:说清做什么 + 用户会怎么说(触发词),一百来字;太泛会被无关任务误触发,太窄永远触发不到。有副作用的流程(部署、发消息)加 disable-model-invocation: true 只允许手动 /name 触发。$ARGUMENTS 接参数,allowed-tools 预授权命令。
- 可预期的追问:怎么测 skill 有没有被触发?用几个自然语言说法试,看模型是否读了正文;再追问「skill 与 subagent 的区别」——skill 是在当前上下文里加载一份说明书,subagent 是另起一个上下文去做事,两者可以组合。
Key points
- Only the description is resident; the body loads on invocation, so body length barely costs
- CLAUDE.md loads in full each time; moving occasional procedures to skills keeps it short
- Write the description as what it does plus how users phrase it, about a hundred words
- Side-effecting workflows get disable-model-invocation; $ARGUMENTS carries parameters
答题要点
- 只有 description 常驻,正文在被触发时才加载;正文长短几乎不影响日常成本
- CLAUDE.md 整份每次加载;偶尔用的流程挪进 skill 是让它继续变短的手段
- description 写「做什么 + 用户会怎么说」,一百来字,不泛不窄
- 副作用流程加 disable-model-invocation;$ARGUMENTS 接参数
What problem do subagents solve? Do they see the main conversation's history? When should you not use one?subagent 解决了什么问题?它看得到主会话的历史吗?什么时候不该用?
Common in ChinaCommon overseasIntermediate#subagents#contextHow to reason about it · think before answering
- The key is the problem solved: protecting the main conversation's context window, not the side benefits of parallelism or specialization. The second part is a common misconception; the third tests judgment.
- Chain: research and review tasks read a lot and keep little — thirty files for one conclusion. Done in the main session, all thirty land in the window and crowd out the actual implementation. A subagent has its own context window, reads everything, and returns only a summary; the main session pays only for the summary.
- Second part: no. A subagent starts with the system prompt, the task you delegated, CLAUDE.md, and a git status snapshot — not the main history, your earlier file reads, or previously loaded skills. That is both a limit and a strength: a reviewer without the memory of having just written the code finds more faults, which is what adversarial review in D5 relies on.
- Configuration: .claude/agents/<name>.md with tools restricting what it may use (no Edit for a reviewer) and model to pick a cheaper or stronger model; built-ins are Explore (read-only), Plan (plan mode research), and general-purpose.
- When not to: tasks needing multi-turn back-and-forth (every dispatch re-explains), phases that share heavy context, and one-line fixes where dispatch plus summary costs more than the work. Follow-up: subagent versus /compact — one keeps content out of the window, the other compresses it afterward; the former is cheaper.
分析过程 · 先想清楚再作答
- 题眼是「解决了什么问题」——答案是保护主会话的上下文窗口,而不是「并行」或「专业化」这些附带好处。第二问是常见误区,第三问考边界感。
- 推导:查资料、审代码这类任务的特征是「读很多、留很少」——读三十个文件只为一段结论。放在主会话里做,三十个文件全进窗口,真正的实现反而没地方放。subagent 拥有独立的上下文窗口,读完只把总结带回来,主会话只付总结的成本。
- 第二问:看不到。subagent 起步时只有系统提示、你派给它的任务描述、CLAUDE.md、git 状态快照;主会话的历史、你之前读过的文件、之前加载的 skill 都不在。这是限制也是优点:一个没有「刚写完这段代码」记忆的审查者更容易挑出毛病,D5 的对抗式审查就靠这个性质。
- 配置:.claude/agents/<name>.md,frontmatter 的 tools 限定它能用什么(审查者不给 Edit)、model 可以配更便宜或更强的模型;内置的 Explore 只读、Plan 用于计划模式、general-purpose 全能。
- 不该用的场景:需要多轮来回讨论的活(每次派出去都要重新交代)、几个阶段要共享大量上下文的活、一句话就能改完的活(交代 + 总结的开销大于任务本身)。可预期的追问:subagent 与 /compact 的关系——一个是不让东西进窗口,一个是进了以后压缩,前者更省。
Key points
- Solves the main window being flooded by read-heavy, keep-little tasks; a subagent has its own window and returns a summary
- It does not see the main history — only the task, CLAUDE.md, and a git snapshot — which makes its review more objective
- tools restricts permissions, model picks the model; built-ins are Explore, Plan, general-purpose
- Avoid for multi-turn discussion, heavy shared context across phases, and one-line fixes
答题要点
- 解决的是主会话上下文被「读很多留很少」的任务撑满;subagent 独立窗口,只带回总结
- 看不到主会话历史,只有任务描述、CLAUDE.md、git 快照;因此审查更客观
- tools 限定权限、model 选模型;内置 Explore / Plan / general-purpose
- 不该用:多轮讨论、多阶段共享上下文、一句话能改完的小活