A bad hook is worse than no hook. It fires at the wrong time, dumps a build log into the context, blocks something harmless, or silently lets the dangerous thing through. And because hooks run automatically, nobody notices until the damage is done.
This lesson builds on Hooks, which covers the mechanics, and The Enforcement Ladder, which covers when to reach for one. Here’s how to write ones that hold up.
Rules for hooks
- Keep scope narrow and output actionable: “Verification failed” is less useful than “The account-settings test failed; here is the command and the first relevant failure.” Avoid dumping an entire build log into every continuation.
- Separate feedback from enforcement: A
PostToolUsecheck can’t prevent side effects that already occurred. That’s not how time works. Likewise, don’t treat an asynchronous check (one that runs in the background without holding the agent up) as a prerequisite for an action that continues before the check finishes. Claude’s documentation explicitly notes that asynchronous hooks can’t block the behavior they would otherwise control. - Make repeated executions safe: Assume a hook may fire more often than you expect. Notifications may need deduplication. File changes should be idempotent (safe to run twice) and converge rather than oscillate. Checks should avoid competing over shared temporary files.
- Don’t assume several hooks form a sequential pipeline: All the hooks that match one event run in parallel, in no guaranteed order. When order matters (format, then check, then report), put the sequence in one script rather than relying on separately registered handlers. Keep the dependencies visible in code.
- Choose failure behavior deliberately: A desktop notification failure should not stop coding from moving forward. A security guard failure probably should. A guard fails closed when it catches its own errors and exits
2(or returns a deny decision) on purpose, because an unhandled crash exits1, which lets the action proceed. Consider timeouts, malformed output, missing executables, and unavailable services, not only successful execution. - Treat hook code as executable software, not harmless configuration: This is a shell script running on your machine. You should probably make sure it’s not going to
rm -rf /or anything like that. - Don’t confuse “it runs” with “it guarantees”: There are four separate properties here. The hook actually gets invoked, its decision is deterministic (the same input gets the same answer), its effect is safe to repeat, and its run is reproducible (you can replay the recorded event later and get the same result, which needs the policy version and inputs saved). Only claim the ones you’ve actually shown.
- Write the decision as a pure function: Something like
policy(event, stateSnapshot, policyVersion), which gives the same answer for the same inputs. The clock, the network, a mutable branch, and environment variables are all hidden inputs. Record them or remove them.
Example hooks
These are the kinds of jobs hooks do well. Most of them fit one specific event, and none of them tries to manage work.
- Catching a missed verification step before handoff: Sometimes the agent finishes with “implemented and ready,” but it hasn’t run the required checks. A hook can use a sentinel, a marker such as a file on disk whose presence unlocks an action, here meaning the checks ran for this exact set of changes (Sentinels covers the idea), to see whether they have.
- Steering an agent away from generated files: If an agent keeps mucking around with your generated files, a hook can redirect it. If all you need is to block the path, a permission deny rule does that more simply. Use a hook when you want to explain the proper source, or when the decision depends on what’s in the call.
- Loading small amounts of context into the current session: Based on what you’re doing and where, add details like the branch name or the ticket for that branch, programmatically, so the agent doesn’t have to think to run the command itself.
- Notifications and other lightweight operational logging: Tell some external system, like a dashboard, what the agent is doing: started, waiting for you, finished.
More specifically, here’s a catalog. The Trigger column names the lifecycle event, a set point in the agent’s work. PreToolUse runs before a tool, PostToolUse after one, and Stop when the agent ends a turn and waits for you. SessionStart and SessionEnd fire at the edges of a session. PermissionRequest fires when the agent is waiting on your approval. (A worktree, mentioned in one row, is an extra checkout of the same Git repository in its own directory.)
| Hook | Trigger | What it does |
|---|---|---|
| Smart formatter | PostToolUse | Formats only files the agent just changed |
| Generated-file guard | PreToolUse | Blocks direct edits to generated code and explains the proper source |
| Dangerous-command guard | PreToolUse | Catches suspicious destructive commands before execution |
| Verification gate | Stop | Checks changed code when the agent stops. Stop also fires when the agent just asks you a question, so the gate has to decide whether the agent is claiming it’s done |
| Context bootstrapper | SessionStart | Injects branch, worktree, environment, issue, and local-service context |
| Attention notification | PermissionRequest | Sends a macOS notification when the agent is waiting for you |
| Secret scanner | PostToolUse | Detects credentials or .env leakage in newly written content, so the agent can remove it. To prevent the write, scan the content in a PreToolUse hook instead |
| Migration guard | PreToolUse | Prevents modifications to already-applied migrations |
| Session cleanup | SessionEnd | Cleans up temporary resources created specifically for the session |
| Telemetry hook | Several, one hook per event | Records tool usage, durations, failures, and token and cost data |
Anti-patterns
Every one of these sounds reasonable until you live with it:
| Proposed hook | Why I’d avoid it | Better choice |
|---|---|---|
“On every UserPromptSubmit, inject our preferred prompt format as extra instructions.” | It silently adds instructions you didn’t write and can distort intent. (UserPromptSubmit fires when you submit a prompt. A hook there can add context, not replace the prompt.) | Clear instructions, or an explicitly invoked planning skill. |
| “After every edit, ask another model whether the architecture is good.” | The review happens at an arbitrary intermediate point and repeatedly evaluates unfinished work. | A review subagent after a coherent change. |
“Whenever a command uses npm, secretly rewrite it to Bun.” (A PreToolUse hook can change a tool’s input.) | It changes the requested operation rather than explaining the project convention. | Instructions first, then a targeted warning or rejection when there’s a concrete incompatibility. |
| “Every session start should install dependencies and recreate infrastructure.” | Merely opening a session becomes an expensive, mutating operation. | An explicit, idempotent setup command, or environment provisioning. |
| “Run a dependency audit every Monday.” | Monday is a scheduling event, not an agent lifecycle event. | A scheduler or a scheduled continuous-integration job. See Routines and Schedules, which covers prompts that run on a schedule or an event. |
| “When one subagent finishes, launch the next five and manage retries.” | Dependencies, cancellation, state, and retry policy become hidden across callbacks. | An explicit coordinator script or workflow runner. See Dynamic Workflows, which covers scripts that orchestrate subagents. |
| “Ensure all contributors obey this check.” | A local agent hook isn’t the shared integration boundary. | Required CI checks and repository protections. |
The pattern across the table is the same. A hook that rewrites, schedules, or coordinates is doing a job that belongs somewhere else.
Next, Hooks in Practice covers testing, rolling out, and the ways guards fail.
A good hook does one small thing, fails the way you chose, and can be explained in a sentence.