Skip to content

Why a hook is stronger than a rule or a permission

You’ve now governed the agent three ways across this course, and the ledger gate you just built sits apart from the other two in a way worth making explicit — because reaching for the wrong one gives you a guarantee weaker than you think you have. Lay them side by side:

  • A rule (project memory, an AGENTS.md line) is context. It tells the model what’s true and what you want, and the model factors it into its decisions. It lives in the context window, which means it can be compacted out, drowned by a long session, or simply not weighed heavily enough on a given turn.
  • A permission (an allow or deny rule) is model-mediated enforcement. It’s stronger than a rule — a deny rule is a hard wall the agent genuinely cannot cross. But it governs the tools the agent may invoke. It answers “may the agent call this?” — not “is the content of this specific commit acceptable?”
  • A hook is deterministic, model-independent code. It doesn’t advise the model and doesn’t sit in the context window at all. It runs on the rail at a lifecycle event, inspects the actual payload, and decides in plain shell. The model’s reasoning, its mood, its remaining context budget — none of it is in the loop.

The sharpest way to feel the difference: a rule and a permission both ultimately route through the model’s judgment or a tool-name match. A hook routes through neither. That’s why the ledger guarantee had to be a hook. “No ledger change without a test” isn’t a fact to remember (a rule) and it isn’t a tool to forbid (a permission) — git commit is a tool you obviously do allow. It’s a condition on the content of a specific action, checked deterministically at the moment that action is attempted. Only a hook can express that.

Stress-test the difference yourself. Four constraints, three homes each — and a clock, because the whole point is that what holds while you watch isn’t what holds at 2am:

Four constraints you want to hold, three homes each: a rule (the model is told), a permission (the harness forbids a class of action), a hook (your code runs on the rail, every time). All four start in the rules file. Re-home them — then move the clock, because what holds while you watch isn’t what holds at 2am.

situation
  • the fragile modulea preference · low stakes

    The auth module is fragile — prefer minimal, surgical diffs there.

    held — you’re the gate

    “Minimal and surgical” is a judgment call, not a checkable condition. You want the model informed and weighing it — and your diff review is the backstop for the times it doesn’t.

  • the secrets walla never · high stakes

    The agent must never read secrets/.

    held — you’re the gate

    An instruction the model weighs — so “never” actually means “unless a debugging trail leads there after the line compacted out.” Walls that matter don’t get to be suggestions.

  • the tested-commit gatea condition on content · high stakes

    No commit that touches money code unless the money tests pass.

    held — you’re the gate

    It works all morning, which is what makes it dangerous. The line compacts out at hour three, and the untested commit lands at 2am with nobody to catch it.

  • the every-time formatteran every-time · low stakes

    Every file the agent writes gets formatted — every time, not most times.

    held — you’re the gate

    The model formats when it remembers, and “every time” done by memory is “most times.” No single miss hurts; the drift and the diff noise pile up.

holding4 of 4shaky0silently broken0

All four look fine — and that’s the trap state. While you watch, an instruction is indistinguishable from a guarantee, because you’re the enforcement. The file didn’t hold the line; you did. Move the clock.

The homes wear each tool’s own names — permissions might be an approvals-and-sandbox dial, a hook might be a plugin subscribing to lifecycle events — but the ladder underneath is the same. Guarantee strength is decided by what sits in the loop: the model’s memory, a harness wall, or your code.

So which do you reach for? The test is about 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 it’s the model deciding.
  • 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 main that skips CI, format every file on write. That’s a hook. You want a guarantee that survives a forgetful agent and a 2am session.

Put plainly: rules and permissions shape what the agent may do; a hook enforces what must be true regardless of what the agent does. The first two trust the agent inside the lines you draw. The hook doesn’t need to trust it at all.

That last property — runs regardless, with nobody in the loop — is exactly why hooks become load-bearing the moment you stop watching the agent work. We’ve built the gate while sitting at the terminal; its real value shows up when you’re not there. Hold that thought.

For now, you’ve assembled the whole kit: the agent has reach (MCP into the database and the board), and it has a gate (the ledger hook that can’t be talked past). But all of it lives in your .claude/ directory, on your machine. Your teammates have none of it. The last move in this chapter is turning this hand-built setup into something you can hand over as a single unit.