Tool Engineering · ~7 min
Lesson 7 made effects safe by designing the tool to be re-runnable. This lesson makes them safe a second way — by intercepting the call before it runs, with code the model cannot reason its way around.
Every prior move lived inside the model's reasoning: a better description, a clearer error, an idempotency guard the tool itself enforces. Hooks step outside it. A hook is deterministic automation the harness — not the model — runs at a fixed point in the request loop, independent of any sampling.
Hooks attach to lifecycle events. The harness invokes a PreToolUse hook before dispatching a tool call
and a PostToolUse hook after the result returns — the same loop, every time. The split is the whole design:
one enforces, one automates.
| Event | When | Role |
|---|---|---|
PreToolUse | Before a tool call dispatches | Enforces — exit 2 blocks the call |
PostToolUse | After the result returns | Automates — lint, format, audit |
UserPromptSubmit | User submits, before processing | Enforces — can block |
SessionStart / Stop | Session or turn boundary | Snapshot, notify, gate |
Hook input arrives on stdin as JSON, so scripts pipe it through jq to read fields like
tool_input.command. For PreToolUse, exit code 2 cancels the call and feeds stderr back to the
model as the reason to adapt. For post-tool events the action already ran, so exit 2 only surfaces feedback — it can't
un-ring the bell.
This is the lesson's core judgment. Reach for a hook only when all three hold; otherwise the rule stays in the prompt.
| Use a hook when… | Use a prompt when… |
|---|---|
| Compliance is non-negotiable — failure has real cost | Guidance is contextual ("prefer X when in Y") |
| The rule is binary — a call either violates it or doesn't | Applying the rule needs model judgment |
There's a strong opposing training prior (e.g. npm over pnpm) | Over-blocking's false positives cost more than rare misses |
Hooks see parameters, not intent. They can't tell a legitimate git push --force on a personal
branch from a dangerous one aimed at main — so architectural guidance, quality standards, and situational judgment stay
in the prompt, where the model evaluates context a hook can't inspect. This is the same line Lesson 6 drew between a
prescriptive sequence and high-level prompting, now at the enforcement boundary.
A hook buys two things a prompt cannot. First, immunity to prompt injection: an injected
instruction can influence what the agent tries, never what a PreToolUse hook allows — the
gate fires before execution, outside the reasoning loop. Second, it has zero context cost:
moving an absolute rule out of CLAUDE.md and into a hook frees the tokens it occupied and removes one more
instruction competing for attention.
Determinism at the tool-call boundary is not determinism everywhere. A hook that you trust to be absolute can be quietly evaded:
Bash(rm *) and the model deletes via a Write that truncates the file, or perl -e 'unlink(...)'. Each call is judged alone, so a denied path has a sibling.sudo from the suspect one.So the deny must cover every tool that achieves the same effect, and the truly hard boundary belongs in OS-level controls — file permissions, network policy, containers — not a single matcher.
PreToolUse enforces, PostToolUse automates: exit 2 blocks pre-tool calls and redirects via stderr.CLAUDE.md.Retrieval practice — recall, don't peek
Question 1A hook is deterministic because it is run by the…
Question 2To cancel a tool call from a PreToolUse hook, the script must…
Question 3A rule belongs in a prompt rather than a hook when applying it…
Question 4A hook that blocks rm but not a truncating Write has failed at the boundary of…
Question 5 · spaced recall from Lesson 12The frontmatter field that makes a side-effect skill user-only, so the model can't fire it on its own timing, is…
rm blocks rm node_modules and breaks more than it guards? Next
in Part 6: The Unix CLI as a Tool Interface — when one run(command) tool replaces the catalog.