A hook can stop an edit before it changes a file. While this is not a perfect way to protect files, it does sometimes prevent mistakes, and is worth implementing at times.
If we want to reject a particular write, we need to inspect the proposed tool input before the tool executes. A PreToolUse hook runs at exactly that point, before the file changes. A check after the write can report what happened, but by then it is too late to prevent that call.
Suppose the maintainer manages release versions separately. We want the agent to change the application code, but leave config slash release dot json to the maintainer. We register a PreToolUse hook with the matcher "Edit or Write". Before either tool runs, the handler reads the proposed file path and compares it with the protected path.
Normalize the path before comparing it, because different spellings can refer to the same file. A relative path, a path with a dot dot step, and the absolute path all resolve to the same file in the project. Our handler resolves the path and checks that it lies inside the project. A path it cannot interpret, or one outside the project, is refused with a reason of its own. The source file "counts dot py" resolves to a different path, so this hook leaves it to the normal permission flow.
For an edit to the protected file, the handler prints this JSON to standard output. Claude Code reads the PreToolUse response and rejects the tool call because permissionDecision is "deny". The file stays unchanged.
The reason tells the agent which file it cannot edit and asks it to contact the maintainer instead. For other files, the handler prints nothing, so Claude Code continues with its normal permission checks. An explicit allow would approve the call through this hook, so we leave the decision unset when this file rule does not apply.
In this recorded run, the agent tried to change the release version from "one point four point zero" to "one point five point zero". Claude Code rejected the edit and returned the hook's reason to the agent. The file kept its old version, and the agent told the user that it had not made the change.
Our matcher covers Edit and Write. A Bash command can still write the same file without passing through this handler, for example by redirecting output or running a script. Searching the command for a suspicious word cannot tell us everything that script will do.
This hook is useful for keeping ordinary edits out of the release configuration. If the process must be unable to write that file through any tool, enforce that restriction with filesystem permissions or a sandbox.
So far, we have checked whether a proposed edit is allowed. Now let's look at a different use case for hooks. When the agent finishes responding, a Stop hook can check the project and ask the agent to keep working. This does not undo earlier edits. It gives the agent a reason to continue, instead of ending the turn with a check that is known to fail.
Our Stop hook checks whether the project passes its checks when the agent tries to finish. The handler runs "bash tools slash check dot s h" on the current files, with a timeout of two minutes. This example uses the project's existing checking script. A hook can also ask a model for a judgment. That takes another model call, and its answer can vary, so use a program when it can make the decision.
The script is short. It runs the tests, the type checker, the linter and the format check, in that order. In this simple sequential script, "set dash e" stops execution when a command fails, and that failure becomes the script's result. If every command succeeds, the script succeeds.
If the check passes, the handler prints nothing and does not block completion. If the check fails, the handler returns a block decision with a reason for further work. Our handler copies the failing lines of the test output into that reason, here the assertion that zero is not equal to five. Notice that this response uses a different syntax from the PreToolUse permission decision. Use the syntax for the event you are handling.
What happens if the agent cannot fix the failure? In this example, the hook allows one correction attempt, then checks again. If the second check still fails, it ends the automatic retry and reports the unresolved problem to the user. A timeout or a broken checking environment also needs attention from a person, so those failures go straight to a warning.
How does the handler know this is a retry? Claude Code sends event information as JSON to the handler's standard input. The excerpt on the left shows the stop hook active field set to true. Claude Code sets this flag when the agent is already continuing after a Stop hook blocked completion. The handler reads the flag. It does not need to keep its own retry counter for this example. Our handler still runs the project check again. If it fails while this flag is true, the handler warns the user instead of returning another block decision. That is how we limit the automatic retry.
On a repeated failure, the handler prints this JSON with a systemMessage. Claude Code displays that warning to the user, and the turn ends. The warning says "the checks still fail" and points to the saved log. Ending the turn does not mean the work passed.
You have seen how a hook can reject an Edit or Write call to a protected file, or ask the agent to continue after a failed check. It would be nice to have a PreToolUse hook on the fridge at midnight. It would deny the request, with the reason that you already had dessert. See you in the next video.