You write in CLAUDE.md: “never edit package-lock.json”. Claude follows it almost always. But “almost” is exactly the problem: a rule in memory is advice the model can miss in a long session.
A works differently. It is your own command that Claude Code runs at a set moment in the session, every time — whatever Claude decides.
Hook events
A hook is attached to an event. The main ones:
| Event | When it fires |
|---|---|
SessionStart | when the session starts |
UserPromptSubmit | when you send a request |
PreToolUse | before every tool call |
PostToolUse | after a successful tool call |
Stop | when Claude finishes answering |
Notification | when Claude Code needs your attention |
Today we care about PreToolUse — the hook that can stop an action.
How PreToolUse blocks
Before a tool is called, Claude Code runs your script and feeds it JSON describing the event — which file is about to be edited, for example. The exit code decides what happens next:
- 0 — all clear, the action goes ahead;
- 2 — the call is blocked, and whatever the script printed to stderr goes to Claude as the reason.
Claude sees the explanation — say, “Blocked: package-lock.json is protected” — and changes its plan.
One important detail: PreToolUse fires before the permission mode is checked. So the block holds in every mode, Auto included.
Registering a hook
Hooks are declared in a settings file, next to the rules from the previous level:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-files.sh", "args": [] }
]
}
]
}
}
matcherpicks the tools:Edit|Writemeans file edits and writes only;${CLAUDE_PROJECT_DIR}fills in the project path, so the hook is found from anywhere;- with
"args": []the script runs directly, without a shell, and the path does not need quoting.
Inside a session, /hooks shows which hooks were picked up.
You can test the script without Claude: feed it the JSON Claude Code would send and look at the exit code.
echo "{\"tool_input\":{\"file_path\":\"$PWD/package-lock.json\"}}" | .claude/hooks/protect-files.sh; echo "exit $?"
If you get exit 2, the hook will fire in a session too.
Quick check
A hook on Edit|Write protects .env. Claude ran the command echo X >> .env. Does the hook stop it?
Careful
Hooks run with your user’s rights: in an interactive session, after you have trusted the folder, and in claude -p without any question at all. Before you trust someone else’s repository, or run Claude in it from a script, read its hooks.
Practice in the terminal