Course · Claude Code · Extending Claude Code
Why a hook is stronger than a rule or a permission
Claude Code gives you three ways to govern what an agent does: a rule (a line in CLAUDE.md or .claude/rules/ that the model reads), a permission (an allow or deny entry in settings.json that the harness - Claude Code itself, rather than the model - evaluates), and a hook (a handler that runs at a fixed moment in the agent’s lifecycle). They are not three strengths of the same thing, and reaching for the wrong one leaves you holding a guarantee weaker than you think. Lay the three side by side and ask of each: who checks, and when?
The running example is a small TypeScript payments service whose CLAUDE.md says to use integer cents and never floating point for currency, and whose src/payments/ledger.ts counts the money. The gate lesson builds a PreToolUse hook that refuses any commit touching that ledger file unless a test changed too; this page explains why that had to be a hook.
A rule is context, and context is weighed
Section titled “A rule is context, and context is weighed”A rule - the lines in CLAUDE.md, an entry in .claude/rules/ - is context. It tells the model what is true and what you want, and the model weighs it. Compaction (Claude Code summarizing a long conversation to reclaim window space) does not delete your project-root CLAUDE.md or unscoped rules files; they are re-injected from disk after a compaction pass (documented here). What can drop out is the finer-grained material: a rule scoped to specific paths, or a CLAUDE.md nested deeper in the tree, stays summarized away until a matching file is read again.
And independently of compaction, weighing is the model’s job. A rule present in context can still lose the turn to a strong prior, and the example repo shows it in one file. The cents rule sits in the window while src/payments/processor.ts computes the fee-inclusive total, amount * 1.029, in floating point (the fee is amount * 0.029; the total adds it to the amount). The rule is loaded, in full, and the code floats anyway.
A permission is a harness wall around a class of action
Section titled “A permission is a harness wall around a class of action”A permission - an allow or deny rule - is Claude Code enforcement. It is evaluated by the harness against the requested tool and input, not by the model - stronger than context: a deny rule is a wall while the permission layer is active. But its question is “may the agent call this?” It governs tools and input classes, and cannot ask “is the content of this specific commit acceptable?” - git commit is a tool you allow, so it waves commits through regardless of content.
A command hook enforces a condition on the real action
Section titled “A command hook enforces a condition on the real action”A command hook is deterministic, model-independent code on the rail - the fixed machinery around the model, not the model itself. It does not advise the model and does not sit in the context window. It fires at the lifecycle event, inspects the actual payload - the staged files, the command string - and decides in plain shell. The model’s reasoning, its remaining context budget, none of it is in the loop.
The word command is doing work in that sentence. Hooks have five handler types - command, http, mcp_tool, prompt, agent - and the last two hand the decision to a Claude model, for when you want judgment instead of a guarantee; the hooks reference catalogs all five. The ledger gate is a command hook because its whole value is that no model, anywhere, decides whether it fires or what it sees.
That is why “no ledger change without a test” had to be a hook. It is not a fact to remember - that is a rule, and CLAUDE.md already remembers it. It is not a tool to forbid - that is a permission, and git commit must stay allowed. It is a condition on the content of a specific action, checked deterministically at the moment that action is attempted.
Stress-test the difference yourself. Four constraints, three homes each - and a clock, because what holds while you watch is not what holds unattended. The ladder is a concept model, not vendor data: rule cells fail when the rule is outweighed, permission cells hold for a named class of action, hook cells hold on the condition itself - and the clock’s compacted state stands for attention drift and for the path-scoped or nested rules that compaction does summarize away, not for deletion of your project-root rules file, which compaction re-injects from disk:
The decision rule
Section titled “The decision rule”So which do you reach for? The test is what kind of guarantee the situation demands:
- State a preference or a fact the agent should weigh - “we use pnpm,” “the auth module is fragile, be careful.” That’s a rule. You want the model informed, and you accept that the model decides.
- Forbid a whole class of action - never read
secrets/, never touch prod config. That’s a permission deny. You want a wall around a tool or a path, no judgment involved. - Enforce a condition that no instruction can be trusted to hold, checked against the real action - no ledger commit without a test, no push to
mainthat skips CI, format every file on write. That’s a command hook. You want a guarantee that survives a forgetful agent and an unattended session.
Put plainly: rules and permissions shape what the agent may do; the gate enforces what must be true regardless of what the agent does (a rule is context; permissions and hooks are control). A rule relies on the agent’s judgment; a permission walls off a named class but leaves an allowed action’s content unexamined; the command hook checks the action itself.
A gate like the ledger hook lives in your repo’s .claude/ directory, beside any MCP servers you registered (external-tool connections) and any skills you wrote (task playbooks). On a second machine none of it exists until someone copies files and rewires them by hand. The last move of this chapter is turning a hand-built setup into something you can hand over as a single unit.