Skip to main content

Writing Good Hooks

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 PostToolUse check 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 exits 1, 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.)

HookTriggerWhat it does
Smart formatterPostToolUseFormats only files the agent just changed
Generated-file guardPreToolUseBlocks direct edits to generated code and explains the proper source
Dangerous-command guardPreToolUseCatches suspicious destructive commands before execution
Verification gateStopChecks 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 bootstrapperSessionStartInjects branch, worktree, environment, issue, and local-service context
Attention notificationPermissionRequestSends a macOS notification when the agent is waiting for you
Secret scannerPostToolUseDetects 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 guardPreToolUsePrevents modifications to already-applied migrations
Session cleanupSessionEndCleans up temporary resources created specifically for the session
Telemetry hookSeveral, one hook per eventRecords tool usage, durations, failures, and token and cost data

Anti-patterns

Every one of these sounds reasonable until you live with it:

Proposed hookWhy I’d avoid itBetter 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.

Last modified on .