Harness Engineering · ~7 min
An instruction is a "should-do" the model can reason around. A hook is a "must-do" the harness runs no matter what the model decides.
"Don't break any links" in your AGENTS.md is a prompt — probabilistic, sometimes ignored.
A link checker that runs on every commit is a guardrail — deterministic, always runs, cannot be reasoned
around. Hooks build the second kind.
The guarantee comes from who runs them. The harness — not the model — invokes a hook at a fixed point in
the request loop, independent of any sampling. A PreToolUse hook fires before the tool call dispatches;
a PostToolUse hook fires after the result returns. The model never gets a vote.
PreToolUse, PermissionRequest, UserPromptSubmit, and
Stop, exit code 2 blocks the action and feeds the hook's stderr back to the model as
feedback. The block is mechanical — the action does not happen.Claude Code fires 25+ hook events across the session, prompt, tool, subagent, compaction, worktree, and file-change lifecycles. The load-bearing few:
| Event | When it fires |
|---|---|
PreToolUse | Before a tool call — exit 2 blocks it |
PostToolUse | After a tool call succeeds — good for format/lint |
UserPromptSubmit | When a prompt is submitted, before processing |
Stop / SubagentStop | When a turn (or subagent) ends — a completion gate (Lesson 06) |
SessionStart / SessionEnd | Session boundaries — load or snapshot state |
Hooks live in settings.json under a hooks object keyed by event name. Each matcher pairs
a tool-name pattern ("Bash", "Write|Edit", or a regex) with a handler. Hook input arrives
on stdin as JSON — scripts pipe it through jq to read tool_input fields.
Three hooks compose a non-negotiable rule set: block dangerous shell commands before they run
(PreToolUse), auto-format every file after it's written (PostToolUse on Write|Edit),
and snapshot progress when the session ends (SessionEnd). None of them depends on the model remembering.
The dividing line is simple. Use a hook when compliance is non-negotiable and the rule is checkable — formatting, security, validation. Use a CLAUDE.md instruction when the rule bends gracefully under load — a style preference, a phrase to avoid. A prompt fails soft and keeps the model in the loop; a hook fails hard and stops it.
Hidden global state: a misbehaving agent is often a hook silently rewriting or blocking files —
hooks are invisible in the transcript unless they exit non-zero, so debugging can take longer than the hook saves.
Over-scoped blocks: a substring check for rm also blocks rm node_modules
and echo "term", pushing users to disable the hook entirely. Performance tax: every
PostToolUse hook runs synchronously on the hot path — a slow formatter inflates every edit. Keep them
tight, scoped, and fast; move heavy work to SessionEnd.
PreToolUse exit 2 to stop; PostToolUse to format/lint.tool_input from stdin via jq — that's the hook contract.Retrieval practice — recall, don't peek
Question 1Hooks are deterministic because they're run by…
Question 2In a PreToolUse hook, exit code 2…
Question 3A hook script reads the tool's arguments from…
Question 4You should prefer a CLAUDE.md instruction over a hook when the rule…
Question 5 · spaced recall from Lesson 02The recommended shape for AGENTS.md is…
settings.json with a secret-scanner and an
auto-formatter, or the full list of which events honor exit-code-2 blocking? Next: Sub-Agents &
Orchestration — isolating work into fresh context windows.