Hooks and controlled local automation
Hooks insert automatic checks when an agent uses a tool. They are powerful because they always act; precisely for this reason they must have strict conditions, timeouts and understandable logs.
What you will tackle, step by step
The life cycle of an event
A hook is an automatic rule linked to an event, for example before or after using a tool. To design it you have to draw the sequence: what triggers it, what information it receives, what decision it produces and what happens next.
An event arises from a requested action. `PreToolUse` can allow, block, or ask for approval before execution. If the tool succeeds, `PostToolUse` runs; if it fails, the distinct event `PostToolUseFailure` exists. Each stage receives a payload and must produce an understandable state.
Draw the timeline first. If you don't know whether a rule should prevent harm or verify an outcome, you don't yet know at what stage to implement it.
- Name the event and the minimum payload that describes it.
- Decide whether the rule should prevent the action or verify its outcome.
- Define observable conditions, decision and exit code.
- Run the tool only if pre-check allows.
- Record the outcome and prevent the hook from reactivating itself.
An agent can initiate staging deployment. You want to prevent unapproved branches and record the outcome without automating production.
- Define event `deploy.requested` with branch, commit, environment and requester identity; exclude tokens and secret variables from the payload.
- In the pre-hook allow only `staging`, verify that the commit exists and request approval if the branch does not follow the release convention.
- If allowed, the tool starts the deployment and returns ID, deployed commit and status; an error keeps `tool_failed` distinct from `blocked`.
- The post-hook reads that result, queries the health check once and records `verified`, `failed` or `timeout`; never starts a production deployment.
Result: The timeline shows who asked what, why the action was allowed, and which version was actually verified. A hang is not confused with a deployment failure.
Pre-hook: Secure the boundary
A pre-hook is a barrier placed before an action. This works well when the rule depends on facts you can measure: resolved path, operation type, environment, or presence of a valid approval.
A pre-hook is suitable for blocking writes to sensitive files, destructive commands, or out-of-scope operations. The decision should depend on observable properties like path and action type, not on vague words in the prompt.
The blocking message must explain which rule was triggered and how to proceed safely. An unremedied blockage transforms a protection into an opaque obstacle. The pre-hook adds a check, but does not replace and cannot bypass the permissions policy.
A workflow can delete artifacts under `/workspace/tmp/build-42`, but must not touch sources, home directories, or paths derived from empty variables.
- The pre-hook receives operation and target, rejects blank or unresolved glob input, and computes the canonical path without performing deletion.
- Compare the target with the explicit root `/workspace/tmp` and ban the root itself; allow only one non-symbolic descendant identified for that build.
- Shows a preview of the files and requests the approval ID for quantities or sizes above the threshold; does not print the contents of the files.
- Test valid directory, `../src`, link to home and empty variable. Only the first case must reach the tool; the others explain the resolved path and the remedy.
Result: Automation cleans up its artifact without turning a path error into a large wipe. Denied cases are understandable and testable.
Post-hook: observe what has already happened
A post-hook works on what really happened. It can make a repetitive check immediate, but it should not hide a substantial correction or make an action that left errors appear successful.
After a successful edit, a hook can format the file, perform a targeted check, replace the output shown to the model, or log the event. The crucial point is temporal: the tool has already acted, so a `PostToolUse` cannot cancel a write or prevent a network call that has already occurred.
To prevent an action use `PreToolUse`. To react to a tool error use `PostToolUseFailure`. In both cases, limit the check to the files involved and return context, duration and message without hiding the real cause from the agent.
The agent changes `src/orders/route.ts`. The project wants consistent formatting and type checking of the API package only.
- The post-hook is activated only after a successful write to a `.ts` file in the API package and receives the exact list of touched files.
- Run the formatter on those files, record whether the diff changed, and do not open other files for general cleanup.
- Run `npm run typecheck --workspace api` with timeout defined; preserves exit code, duration and first diagnostic lines without sensitive data.
- If the check fails, returns `written_but_unverified` and the message; it does not modify types or tests silently and does not undo user writing.
Result: You have repeatable formatting and relevant evidence. A type error remains visible and attributed to the next check, without being confused with a writing failure.
Idempotence, recursion and failure
A reliable hook must behave well even when it is repeated, overlaps with another execution, or encounters a failing service. These cases are not remote exceptions: they are the normal cost of automation.
An idempotent hook can be repeated without multiplying effects. A formatter launched from `PostToolUse` does not automatically generate a new event; the loop arises if the script causes the very event it observes or requests new calls to the same tool. Name that event and use an execution marker or command that doesn't trigger it again.
Define timeout and error policy. For critical protection it can be correct to fail closed; for a non-essential metric it may be preferable to log the problem and move on.
After a change in `docs/`, a hook updates `docs/index.json`. The first prototype is reactivated when it saves the index itself and duplicates the entries.
- Change the generator: reads the source documents, sorts the entries and rewrites the entire index only if the calculated content is different.
- Exclude `docs/index.json` from files that trigger the hook and add a short lock to prevent two simultaneous saves from writing together.
- Execute the same event twice: the first update produces the new index, the second does not change bytes or add entries. Then simulate an interrupted process and verify the release of the lock.
- If generation fails, keep previous index, mark documentation as unverified, and show manual command; do not post a partial JSON.
Result: Consecutive retries and saves produce only one valid index. The generated file does not reactivate its hook and a failure leaves the last full version available.
Protection and formatting
Task Notes contains formattable `.env` and Python files.
PRE write: if path ends with .env → block and explain
TOOL write: apply the authorized change
POST write: if path ends with .py → format that file
LOG: event, decision, duration, exit code; never the secret's contents