Hooks: the one layer that guarantees
A hook that denies blocks the tool even under bypassPermissions. Hooks tighten restrictions and never loosen them, and that is what makes them the policy layer.
- claude code
- hooks

Contents
- The 30 events, grouped by what they serve
- Five types, and two that are not deterministic
- The rule that makes a hook count: tighten, never loosen
- Hooks run in parallel, and that has a consequence
- Where the hook lives defines its reach
- Three hooks worth the effort
- When to skip the hook
- Frequently asked questions
- Sources
CLAUDE.md is context and the agent can ignore it. A skill is context and the agent can leave it uninvoked. A hook is a shell command Claude Code runs at a fixed point in the lifecycle, and it happens whatever the model decides.
That is the difference that matters, and it has a documented proof: a PreToolUse hook returning deny blocks the tool even in bypassPermissions mode, even with --dangerously-skip-permissions.
No other layer of Claude Code does that.
The 30 events, grouped by what they serve
Most writing about hooks covers two events. Thirty exist.
Session lifecycle: SessionStart, SessionEnd, Setup, PreCompact, PostCompact, ConfigChange, InstructionsLoaded.
Turn lifecycle: UserPromptSubmit, UserPromptExpansion, Stop, StopFailure, MessageDisplay, Notification.
Tool lifecycle: PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch, PermissionRequest, PermissionDenied.
Delegation: SubagentStart, SubagentStop, TeammateIdle, TaskCreated, TaskCompleted.
Environment: CwdChanged, DirectoryAdded, FileChanged, WorktreeCreate, WorktreeRemove.
MCP: Elicitation, ElicitationResult.
A few deserve attention because they solve problems people try to solve another way:
InstructionsLoadedfires when aCLAUDE.mdor a.claude/rules/rule enters the context, lazy mid-session loads included. It is the right tool for debugging apathsrule you cannot tell has loaded.PostCompactruns after compaction, the place for reinjecting context that fails to survive/compact.CwdChangedfires when the agent runs acd. It suits reactive environment management, in the style ofdirenv.FileChangedwatches a file on disk, with thematchersetting which names to watch.
Several of those events exist to solve traps that degrade in silence.
Five types, and two that are not deterministic
Here is the part that contradicts the running definition of a hook.
| Type | What it does |
|---|---|
command | Runs a shell command. The default |
http | POSTs the event data to a URL |
mcp_tool | Calls a tool from an already-connected MCP server |
prompt | A single-pass LLM evaluation |
agent | Multi-turn verification with tool access, experimental |
The last two exist for decisions needing judgment rather than a fixed rule.
The prompt hook. Instead of running shell, Claude Code sends your prompt and the event data to a model (Haiku by default), which returns the decision as JSON:
{
"hooks": {
"Stop": [{
"hooks": [{
"type": "prompt",
"prompt": "Check whether every task finished. If not, answer {\"ok\": false, \"reason\": \"what is missing\"}."
}]
}]
}
}With "ok": false on a Stop event, the reason goes back to Claude as the next instruction and it keeps working. An important escape valve exists: "impossible": true marks the condition as impossible to satisfy, and the turn then ends instead of looping.
The agent hook. When the verification needs to read a file or run a command, type: "agent" creates a subagent that investigates before deciding. A 60-second default timeout and up to 50 tool turns.
{
"type": "agent",
"prompt": "Check whether every unit test passes. Run the suite and check the result. $ARGUMENTS",
"timeout": 120
}The documentation marks agent hooks as experimental and recommends command hooks for production flows.
The criterion between the two: use prompt when the event data suffices for the decision; use agent when you have to check against the code's real state.
The rule that makes a hook count: tighten, never loosen
This is the property that turns a hook into a policy layer.
PreToolUse fires before any permission mode check, in every mode, dontAsk included. A hook returning permissionDecision: "deny" blocks the tool under bypassPermissions and with --dangerously-skip-permissions.
In other words: you can impose a policy the user cannot get around by changing the permission mode. For a team with a compliance requirement, it is the one documented way to do that.
The inverse does not hold. A hook returning allow:
- cannot bypass a
denyrule from the settings - cannot suppress the connector tool prompt the organization marked as
ask - cannot suppress an MCP tool marked
requiresUserInteraction
The asymmetry is deliberate, and it is what makes the mechanism dependable. A hook tightens; it never lets go.
Worth knowing too that exit code 2 blocks, and blocks hard: even JSON carrying permissionDecision: "allow" cannot override an exit 2.
The delegation events (SubagentStart, SubagentStop, TeammateIdle) close the loop described in subagents: when to delegate and when not to.
Hooks run in parallel, and that has a consequence
When several hooks match the same event, all of them run in parallel and all of them complete before Claude Code combines the results.
The consequence that catches people: a hook returning deny stops no side effect from a sibling hook.
The documentation's example is clear. Two hooks on Bash: one records the command in a log and exits 0; the other exits 2 to deny when the command contains rm -rf. Claude tries to run rm -rf /tmp/build and both execute. The command gets blocked, and the log line gets written anyway, because the logging hook already ran.
To combine decisions, the most restrictive answer wins, in the order deny → defer → ask → allow. The additionalContext text, meanwhile, gets preserved from every hook and delivered together.
And a concurrency trap exists: when more than one PreToolUse hook returns updatedInput to rewrite a tool's arguments, the last one to finish wins, and the order is non-deterministic. The official recommendation is to avoid two hooks modifying the same tool's input.
Where the hook lives defines its reach
Seven possible places exist, and the choice decides who inherits the rule.
| Where | Reach | Shareable |
|---|---|---|
~/.claude/settings.json | All your projects | No |
.claude/settings.json | One project | Yes, versionable |
.claude/settings.local.json | One project | No, it stays gitignored |
| Managed configuration | The whole organization | Yes, controlled by the admin |
A plugin, in hooks/hooks.json | Wherever the plugin is active | Yes, alongside the plugin |
| A skill's frontmatter | The rest of the session, after the skill is invoked | Yes, inside the skill's file |
| A subagent's frontmatter | While that subagent runs | Yes, inside the subagent's file |
The last two rows are the least known and the most interesting.
Claude Code registers a hook declared in a skill's frontmatter the moment something invokes the skill, and it keeps applying for the rest of the session. That lets you package policy alongside the procedure: the skill doing the deploy can bring the hook that blocks writes to production.
A hook declared in a subagent's frontmatter applies while it runs and no longer. It is the way to give a subagent a restriction the main session does not carry, and the documentation's example validates a read-only query before letting the subagent touch the database.
To switch everything off, "disableAllHooks": true in the settings. Two caveats: configuration precedence applies, so the project file can override yours; and hooks from managed configuration keep running unless the flag sits there too. That fits the rule that a hook tightens and never loosens.
/hooks lists everything configured, grouped by event, and it is where to start when something fires and you cannot tell where it came from.
Three hooks worth the effort
1. Filter verbose output before it becomes context. The highest-return case. Instead of the agent reading a 10,000-line log to find the errors, the hook filters and returns what matters:
#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')
if [[ "$cmd" =~ ^(npm test|pytest|go test) ]]; then
filtered="$cmd 2>&1 | grep -A 5 -E '(FAIL|ERROR|error:)' | head -100"
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"updatedInput\":{\"command\":\"$filtered\"}}}"
else
echo "{}"
fi2. Block a protected path. A rule that depends on no agent remembering it:
{
"hooks": {
"PreToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-protected.sh"
}]
}]
}
}3. Reinject context after compaction. A PostCompact solves the problem of an instruction vanishing mid-session, since nested CLAUDE.md files and paths rules never reinject themselves.
To check whether a hook is active, /hooks shows whether it appears under the right event. And claude --debug shows modified tool input keys when a hook rewrites the command.
When to skip the hook
When the decision needs conversation context. A command hook communicates through stdout, stderr and an exit code alone. It sees none of the conversation.
When you need to undo. PostToolUse runs after the tool already executed. No rollback exists.
When "always" is not always. Stop fires every time Claude finishes answering, and not when the task finishes. And it does not fire on a user interruption. An API error fires StopFailure, a different event.
When the latency cost outweighs it. The timeouts vary: command, http and mcp_tool get 10 minutes, while UserPromptSubmit drops to 30 seconds and MessageDisplay to 10. A prompt hook gets 30 seconds and an agent one 60. The SessionEnd hooks share a budget of 1.5 seconds.
And one debugging note that saves time: matchers are case-sensitive. Half of the "my hook does not fire" reports are a tool name in the wrong case.
Worth remembering that a hook can arrive from outside: an installed plugin can register its own. Plugins and marketplaces covers what to check first.
Frequently asked questions
Is a hook better than an instruction in CLAUDE.md?
They are different layers. CLAUDE.md guides behavior and the agent can skip it. A hook executes at a fixed point in the cycle, whatever the model does. Use CLAUDE.md for conventions and a hook for what needs a guarantee.
Can I stop someone disabling my hooks by changing the permission mode?
Yes, and that is the central property. PreToolUse fires before the mode check, so a deny holds even under bypassPermissions and with --dangerously-skip-permissions. The inverse path does not exist: a hook cannot loosen a settings restriction.
My hook does not fire. Where do I start?
Three checks, in this order: /hooks confirms it appears under the right event; the matcher has to match the tool name character for character, in the same case; and the event has to be the right one, PreToolUse before and PostToolUse after.
Does a prompt hook make the mechanism non-deterministic?
It does, and that is intentional. The prompt and agent types exist for decisions needing judgment. For a deterministic guarantee, the type is command with an exit code, and that is what the documentation recommends for production flows.
Sources
- Anthropic — Claude Code Docs: Automate actions with hooks. Accessed 20 August 2026.
- Anthropic — Claude Code Docs: Hooks reference. Accessed 20 August 2026.
- Anthropic — Claude Code Docs: Manage costs effectively. Accessed 19 August 2026.
- Anthropic — Claude Code Docs: How Claude remembers your project. Accessed 19 August 2026.
- Anthropic — Claude Code Docs: Sub-agents. Accessed 20 August 2026.
Verified on 20 August 2026.
Review trigger: revisit when (a) agent hooks leave the experimental stage, (b) the list of 30 events or the per-type timeouts change, or (c) the precedence between a hook and the permission mode changes, since that is what holds up the use of hooks as a policy layer today.
Read next
Motion •
Motion Design for the Web: The Complete Guide
Scroll, text, images and video: the complete catalog of motion techniques for the web, with implementation in Next.js and the cases where each one pays off.
- motion
- scroll
The definitive guide — a Next.js site built around motion and scroll
The scroll foundation that, when missing, keeps the animations from working at all: Lenis, GSAP and Next.js wired in the right order and the mistakes to avoid.
- next.js
- lenis
Infra •
Documentation: deploying a Next.js application with GitHub + Hostinger
Every push becomes a live site with no hosting panel involved: connecting GitHub to Hostinger, the build settings that break and the checks after each deploy.
- deploy
- github


