Place hook on correct event
What you'll learn
Pick the hook event by what your trigger fires on: blocking → PreToolUse · per-task → Stop · per-tool-call → PostToolUse · once at boot → SessionStart.
- Hooks run in the harness. The model can't refuse, forget, or interpret them.
- PreToolUse is the only blocking event. Guardrails live there, period.
- PostToolUse = per individual tool call. Stop = per task / turn. Pick by granularity.
Hooks in Claude Code are deterministic harness-side actions wired to lifecycle events. The model does not run a hook; the harness does, regardless of what the model decides. That distinction is the entire reason hooks exist: when you need a guarantee — a guardrail, an automated post-task action, a trust gate — the model is the wrong layer to put it on. The hook event you choose decides whether the action fires before, after, or instead of the thing you care about. Picking the wrong event is the single most common hook bug.
The events worth memorising at this tier (and their granularity): SessionStart fires once when a session begins; UserPromptSubmit fires when the user sends a turn; PreToolUse fires before each individual tool call and is the only event that can return a blocking decision to refuse the call; PostToolUse fires after each tool call with its result; Notification fires for transient harness messages; Stop fires once when the assistant finishes a task / turn; SubagentStop fires when a delegated subagent finishes; PreCompact fires before context compaction; SessionEnd fires once when the session closes. Granularity is everything: PreToolUse is per-tool-call; Stop is per-task.
The decision rule maps directly from the trigger phrasing. 'Refuse / block / require approval before X tool runs' → PreToolUse (the only event with a block primitive). 'Every time you finish a task, do X' → Stop (per-task granularity matches 'task'). 'Every time a tool call completes, do X' → PostToolUse (per-call granularity matches 'each call'). 'When the session boots, prepare X' → SessionStart. 'Every time the user sends a message, validate X' → UserPromptSubmit.
Two anti-patterns produce most real-world miswires. First: putting a guardrail on PostToolUse instead of PreToolUse. PostToolUse can warn, but the dangerous command has already executed — you've turned an enforcement into a forensic alert. Second: putting a per-task automation on PostToolUse instead of Stop. PostToolUse fires per-tool-call, so 'run lint after the task' becomes 'run lint after every Edit, every Read, every Bash' — a flood, not a checkpoint. Match event granularity to the trigger granularity, not just the verb.
Key points
- Hooks run in the harness. The model can't refuse, forget, or interpret them.
- PreToolUse is the only blocking event. Guardrails live there, period.
- PostToolUse = per individual tool call. Stop = per task / turn. Pick by granularity.
- Trigger phrasing maps to event: 'before tool' → PreToolUse · 'after task' → Stop · 'after each call' → PostToolUse · 'on boot' → SessionStart.
- If the placement could fire at the wrong granularity, you've picked the wrong event.
Examples
Trigger: 'block any shell command containing rm -rf or --no-verify, regardless of permission mode.' Required primitive: blocking decision before execution. Event: PreToolUse. Hook inspects the candidate command; if it matches either pattern, it returns a deny decision and the call never runs. PostToolUse can't do this — the command has already run.
Trigger: 'every time you finish a task, run npm run lint.' 'Task' granularity = per-turn = Stop. PostToolUse would fire after each individual Edit, Read, and Bash call — running lint dozens of times per task. Stop fires once when the assistant signals it's done with the user's request. Match granularity to the user's words.
Trigger: 'when a session starts, dump current git branch + uncommitted file count into the conversation as context.' One-shot, at session begin. Event: SessionStart. Not UserPromptSubmit (that runs every turn — wasteful repetition). Not Stop (too late).
Pitfalls
- PostToolUse for guardrails — fires after the dangerous command. No block primitive at that event.
- PostToolUse for per-task automation — wrong granularity; lint floods on every tool call.
- Stop for guardrails — fires only at task end, way too late to prevent harm.
- SessionStart for per-turn validation — fires only once at boot; misses every subsequent turn.
- Putting an automation that 'must always run' in CLAUDE.md or memory instead of a hook — the model can interpret, prioritize, or skip those. The harness can't skip a hook.
Source notes: 00-academy-basics/notes/03-claude-code-101.md
Ask Claude
Build a prompt with this lesson + your question, copy it, and open the Claude Project in a new tab.