Hooks — Control Layer: Deterministic Guardrails for Agents
Throughout the previous two articles, we have encountered a recurring warning: CLAUDE.md and Skills are guidelines — Claude tries to follow them but does not guarantee absolute adherence. So when you need something to definitely happen — always format code after editing, always scan for secrets before committing, absolutely block data deletion — what can you rely on?
The answer is Hooks. This is the layer that transforms "wants" into "guarantees."
What is a Hook: The Core Difference
A Hook is a command or script defined by you, running deterministically at fixed points in the lifecycle of the tool and the conversation of Claude Code. The key point: the model does NOT decide whether the hook runs or not — when the event occurs and the conditions match, the hook always runs (Claude Docs — Hooks guide).
Consider the comparison:
- Writing "always run tests before committing" in
CLAUDE.md→ Claude usually remembers, but sometimes forgets. - Setting a hook to run tests → it always runs, regardless of the model's mood.
This is why Anthropic categorizes hooks (along with permissions/managed settings) into the "real guardrails" group, separate from the "guidelines" group (Anthropic — Steering Claude Code). Think of hooks as Git hooks for your agent.
In addition to shell commands, a hook handler can also be an HTTP endpoint, an MCP tool, a prompt sent to the model, or a checking agent (Claude Docs — Hooks).
Hook Events
Hooks are attached to events named in PascalCase. The "classic" set you will encounter most often:
| Event | Triggers When | Can Block? |
|---|---|---|
PreToolUse | Just before a tool runs | ✅ (block tool) |
PostToolUse | After the tool returns a result | ❌ (event has occurred) |
UserPromptSubmit | When the user submits a prompt | ✅ (reject prompt) |
SessionStart | At the beginning of a session | — (initialization) |
SessionEnd | At the end of a session | — |
Stop | When Claude intends to stop the turn | ✅ (prevent stop) |
SubagentStop | When a subagent stops | ✅ |
Notification | When Claude sends a notification | ❌ |
PreCompact | Before compressing context | ✅ |
The current documentation also lists many other events (the list expands with versions), so when implementing, be sure to check the exact version of Claude Code you are using (Claude Docs — Hooks).
They can be grouped into three "rhythms": once per session (SessionStart/SessionEnd), once per turn (UserPromptSubmit/Stop), and each time a tool is called in the agentic loop (PreToolUse/PostToolUse).
Where to Configure Hooks
Hooks are declared in the JSON settings file, with a three-level nested structure: event → matcher group → handler list. The file's location determines the scope (Claude Docs — Hooks):
~/.claude/settings.json— for all projects (personal machine)..claude/settings.json— for one project, can be committed to the repo (shared with the team)..claude/settings.local.json— for one project, gitignored (for your use only).- Managed policy settings — for the entire organization (set by admin).
- In a plugin:
hooks/hooks.json.
Typical structure:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "/path/to/lint-check.sh" }
]
}
]
}
}Matcher: be careful with regex
matcher determines which hook runs for which tool:
"*","", or left blank = matches all.- Only includes letters/numbers/
_/-(and|,,for listing) = matches exact string. - Contains other characters = unanchored JavaScript regex.
Classic trap: Edit (as regex) will match NotebookEdit. To match exactly, wrap with ^...$. For the MCP tool, the matcher filters by names in the format mcp__<server>__.*.
Input, output, and exit code
Claude sends a JSON describing the context to the hook (via stdin with command, or POST body with http). Common fields include session_id, transcript_path, cwd, permission_mode, hook_event_name; tool events additionally have tool_name, tool_input (Claude Docs — Hooks).
Exit code is the simplest control mechanism:
exit 0= success. If the hook outputs JSON to stdout, Claude will read that JSON.exit 2= blocking error. stdout is ignored; stderr is returned to Claude. Consequences depend on the event:PreToolUseblocks the tool call,UserPromptSubmitrejects the prompt,Stopprevents Claude from stopping... (whilePostToolUse/Notificationcannot block since the action has already occurred).- Other exit codes = non-blocking errors.
To control more intricately than the exit code, the hook can output JSON with fields like continue, systemMessage, suppressOutput, decision/reason. Specifically, PreToolUse supports hookSpecificOutput.permissionDecision receiving allow/deny/ask/defer — allowing "to ask the user again" before the tool runs.
Two practical examples
(a) Auto lint/format after file edits — PostToolUse matches Edit|Write, calling the format script. No need to block anything, just clean up after each edit.
(b) Block dangerous commands — PreToolUse matches Bash, the script reads the command from JSON and blocks it if it sees rm:
#!/bin/bash
command=$(jq -r '.tool_input.command' < /dev/stdin)
if [[ "$command" == rm* ]]; then
echo "Blocked: rm command is not allowed" >&2
exit 2
fi
exit 0This is the example of "blocking rm -rf" in the toolchain diagram — and it definitely blocks, because the hook is deterministic.
Best practices
- Keep hooks at the start of the session/turn fast.
UserPromptSubmithas a timeout (around 30 seconds) and blocks prompt processing — do not perform heavy work here. - Long tasks should run in the background. With
type: "command", set"async": trueto let Claude continue while the hook (test suite, deploy) runs in the background. - Use path variables.
${CLAUDE_PROJECT_DIR}and${CLAUDE_PLUGIN_ROOT}help scripts not depend on the current directory. - Place correctly. Static context should be in
CLAUDE.md; hooks are for actions based on events.
Security risks — read this section carefully
This is the most powerful and dangerous layer. The official documentation states: hooks run arbitrary shell commands with the user's full permissions (Claude Docs — Hooks). A malicious or careless hook can delete files, leak data, or compromise the system.
Safety Principles (according to the documentation):
- Validate and sanitize all inputs before use.
- Always quote variables:
"$VAR", do not leave$VARunquoted. - Prevent path traversal (
..), use absolute paths. - Avoid touching sensitive files:
.env,.git/, keys/credentials. - At the organizational level, admins can enforce that only approved hooks are allowed to run (managed hooks).
For businesses: treat hooks like infrastructure code — review, versioning, and limit trusted sources, just like you do with Git hooks or CI/CD.
Common Misunderstandings to Avoid
- "Hook is a suggestion for the model." — Incorrect. A hook is a guarantee, distinctly different from a skill/prompt.
- "exit 2 always blocks." — Incorrect. The outcome depends on the event;
PostToolUse/Notificationcannot be blocked. - "matcher
Editonly matches Edit." — Incorrect. It is a non-anchored regex, matching bothNotebookEdit.
Summary & Next Steps
Hooks are a control layer: they run deterministically based on lifecycle events, configured through JSON settings (scope depending on file location), controlled by exit codes (especially exit 2 to block), and are the only "real guardrail" aside from permissions. In return for that power, there is a high responsibility for security.
So far, we have been discussing one agent with memory, skills, and control. But one agent should not handle everything — just like a company needs multiple roles. Article 4 introduces Subagents: delegating tasks to a team of AI assistants, each with its own context and permissions.
References
- Claude Docs — Automate actions with hooks (hooks guide): https://docs.claude.com/en/docs/claude-code/hooks-guide
- Claude Docs — Hooks reference: https://docs.claude.com/en/docs/claude-code/hooks
- Anthropic — How to configure hooks: https://claude.com/blog/how-to-configure-hooks
- Claude Docs — Intercept and control agent behavior with hooks (Agent SDK): https://code.claude.com/docs/en/agent-sdk/hooks
- Anthropic — Steering Claude Code (guidelines vs guardrails): https://claude.com/blog/steering-claude-code-skills-hooks-rules-subagents-and-more
Article 3/7 in the series "Agent Development Toolkit with Claude". Content reinterpreted from the original documentation by Anthropic. Previous ← Article 2: Agent Skills · Next → Article 4: Subagents.
