How Claude Code Hooks Work - Events, Matchers, and the PreToolUse Enforcement Point
- Hooks are user-defined handlers - shell commands, HTTP endpoints, MCP tools, or prompts - that Claude Code runs automatically at fixed points in its lifecycle, configured under the hooks key in settings.json.
- Events cover the whole session: SessionStart, UserPromptSubmit, PreToolUse and PostToolUse around every tool call, Stop when a turn ends, plus notification, compaction, and config-change events.
- PreToolUse is the enforcement point: a hook sees the exact tool call as JSON before it runs and returns allow, deny, or ask, deterministically - no prompting or model goodwill involved.
- Decision control works through exit codes (0 with JSON output, 2 to block) and structured JSON fields like permissionDecision and updatedInput.
- Hooks are also an attack surface: they execute with your full user permissions, and a hook in a repo's committed settings arrives with a git clone.
- Anomity enforces at exactly this boundary - its Endpoint Sensor evaluates each tool call at the agent hook and returns allow, deny, or log before it runs - and inventories hooks as one of the eight AI artifact types.
Most of what people write about securing coding agents amounts to asking the model nicely. Claude Code hooks are the other thing: a documented mechanism for running your own code at fixed points in the agent's lifecycle, with the power to block, rewrite, or annotate what the agent does - deterministically, at the application layer, regardless of what any prompt says. If you operate Claude Code across a team, hooks are simultaneously your best enforcement primitive and a config surface you need to govern. This guide covers both, based on the current hooks reference.
What a hook is
A hook is a handler registered for a lifecycle event. When the event fires, Claude Code invokes the handler with a JSON payload describing what is happening - session id, working directory, permission mode, and event-specific fields like the tool name and its full input. Handlers come in several types: command (a shell command or executable, JSON on stdin), http (the payload POSTed to a URL), mcp_tool (a tool call on a connected MCP server), prompt (a single-turn model evaluation returning a yes/no decision), and an experimental agent type that spawns a subagent to verify a condition. All matching hooks for an event run in parallel, duplicates are deduplicated, and each has a timeout (600 seconds by default for commands).
The lifecycle: where hooks fire
The event catalog is large - it now covers notifications, subagent start and stop, context compaction, config changes, file watches, and worktree lifecycle - but a handful of events do most of the work in practice:
| Event | Fires | Typical use |
|---|---|---|
| SessionStart | When a session begins or resumes | Inject context (branch, open issues), set up the environment |
| UserPromptSubmit | When a prompt is submitted, before the model sees it | Validate or enrich prompts; block disallowed requests |
| PreToolUse | Before every tool call executes | Policy enforcement: allow, deny, ask, or rewrite the call |
| PostToolUse | After a tool call succeeds | Auto-lint and format, validate results, feed context back |
| Stop | When Claude finishes responding | Verify the work is actually done; block premature stops |
| Notification / SessionEnd | On notifications and session close | Alerting, cleanup, logging |
Anatomy of a hook configuration
Hooks live under the hooks key in settings. Each event maps to a list of matcher groups; each group carries the handlers to run when the matcher fits. A matcher is a plain string for exact tool names (Bash), a pipe list (Edit|Write), or a regex (mcp__memory__.*) - which is also how you scope hooks to MCP tools, since those are named mcp__server__tool. Tool events additionally accept an if condition in permission-rule syntax, like Bash(git *), evaluated against the actual command.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
"args": [],
"timeout": 30
}
]
}
]
}
}
Where a hook is defined determines who controls it, and the scopes stack from personal preference to fleet policy:
| Location | Scope | Governance note |
|---|---|---|
| ~/.claude/settings.json | One user, all projects | Personal; invisible to the team |
| .claude/settings.json (committed) | Everyone who clones the project | Shared - and arrives with a git clone, trusted or not |
| .claude/settings.local.json | One user, one project | Gitignored; escapes review by design |
| Managed policy settings | Organization-wide | Admin-controlled; can set allowManagedHooksOnly to block all other hooks |
| Plugin hooks.json / skill frontmatter | While the component is active | Third-party code registering handlers on your machine |
Decision control: how a hook says no
Command hooks communicate through exit codes. Exit 0 means success, and stdout is parsed for JSON directives. Exit 2 is a blocking error: the action is stopped and stderr goes to Claude as the explanation. Any other exit code is a non-blocking error. The richer channel is JSON on exit 0. For PreToolUse, the hook returns a permissionDecision - allow approves the call, deny blocks it with a reason the model sees, ask escalates to a user prompt, defer falls through to the normal permission flow. The same JSON can rewrite the tool's arguments with updatedInput (for example, swapping a risky command for a safe one) or attach additionalContext for the model. Universal fields like continue:false stop the agent entirely, and systemMessage surfaces a warning to the user.
A minimal deny hook makes the mechanics concrete. This script reads the proposed Bash command from stdin and refuses destructive deletes, with a reason that goes back to the model:
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q 'rm -rf'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Destructive rm -rf blocked by policy"
}
}'
else
exit 0
fi
Why PreToolUse is the enforcement point that matters
Everything an agent does to the world - run a command, edit a file, call an MCP tool, make a request - is a tool call, and every tool call passes through PreToolUse before it executes. That makes this one event categorically different from the rest of the catalog. Instructions in CLAUDE.md can be ignored or poisoned; permission rules are static patterns; but a PreToolUse hook is code that inspects the exact action and rules on it, every time, even when the agent has been steered by prompt injection. It is where an injected read-secrets-and-exfiltrate chain actually breaks: the injection can put intent into the model, but the tool call still has to get past the hook. This layering - rules for cheap static cases, hooks for real policy - is the architecture we recommend in the Claude Code permissions and hooks hardening guide, and it is why bypassing the permission layer entirely is such a consequential choice, as covered in what dangerously-skip-permissions actually does.
Hooks as an attack surface
The same properties that make hooks a great control make them a target. A hook executes with your full user permissions, on every matching event, with no per-run prompt. Three arrival paths deserve attention. First, repo-borne hooks: a committed .claude/settings.json means cloning a repository can install handlers that run on your machine - the same class of problem as project files that execute on open. Second, component-bundled hooks: plugins and skills can register hooks in their manifests and frontmatter, so a malicious skill is also a hook-delivery vehicle. Third, persistence: an agent that has been injected once can write a hook that reloads on every future session, outliving the compromised conversation. The operational answer is the same as for every artifact in the agentic stack: inventory which hooks exist on which endpoints, know where each came from, treat hook changes as events worth reviewing, and use managed settings with allowManagedHooksOnly where fleet policy demands it. The audit side of that story is covered in auditing Claude Code across a fleet.
How Anomity uses this boundary
Anomity's runtime governance is built on exactly the mechanism this guide describes. On agents that expose a hook - Claude Code's PreToolUse is the canonical example - the lightweight, unprivileged Endpoint Sensor evaluates each tool call against organizational policy and returns allow, deny, or log before it runs, fleet-wide and centrally managed instead of per-laptop shell scripts. Hooks themselves are one of the eight AI artifact types the sensor discovers and inventories, alongside agents, MCP servers, extensions, plugins, skills, secrets, and CLIs, with source, owner, and version for each - so a new or modified hook on any endpoint surfaces as a change event rather than a silent addition. The sensor sends metadata only, never source or prompts, violations route to SIEM, Slack, email, and Jira, and every decision lands in a queryable 90-day audit trail. Rolling this out across a team is covered in deploying Claude Code across a fleet.
You can't govern what you can't see.The Anomity principle
The bottom line
Hooks are the most underused security feature in Claude Code. They turn policy from a request into a guarantee: every prompt, every tool call, and every turn can pass through your code, and PreToolUse in particular gives you a deterministic allow, deny, or ask on every action the agent takes. Use them - and govern them, because a hook is executable config that arrives through repos, plugins, and skills, and runs with everything you have. Start with a deny hook for the commands you never want an agent to run, layer permission rules underneath, and put hook inventory on the same footing as the rest of your AI artifact inventory. To see every hook, agent, and tool call across your fleet, book a 30-minute demo.
Frequently asked questions
What are Claude Code hooks in plain terms?
Hooks are pieces of your own logic that Claude Code runs automatically at fixed points in its lifecycle: when a session starts, when you submit a prompt, before and after every tool call, and when the agent finishes a turn. A hook can be a shell command, an HTTP endpoint, an MCP tool call, or a prompt evaluated by a model. Because hooks execute deterministically at the application layer, they turn suggestions like "always run the linter" into code that always runs.
What is the PreToolUse hook and why does it matter for security?
PreToolUse fires before any tool call executes - a shell command, a file edit, an MCP tool. The hook receives the full tool call as JSON on stdin and can return a permissionDecision of allow, deny, or ask (or defer to the normal permission flow), with a reason the model sees. It is the one point where every action the agent wants to take passes through your code before it happens, which makes it the natural enforcement point for policy - and the hook Anomity uses for runtime governance.
How do hook matchers work?
Each hook entry has an optional matcher evaluated against the event's subject - the tool name for tool events, the notification type for Notification, and so on. A plain string like Bash matches exactly, Edit|Write matches either, and anything with other characters is treated as an unanchored regex, so mcp__memory__.* matches every tool from one MCP server. An omitted matcher or "*" matches everything. Tool-event hooks can add an if condition using permission-rule syntax, like Bash(git *), to fire only on matching commands.
How does a hook block an action?
Two ways. A command hook can exit with code 2, which blocks the action and feeds its stderr to Claude as the error message. Or it can exit 0 and print JSON: for PreToolUse, hookSpecificOutput.permissionDecision set to deny blocks the call with a reason, ask escalates to the user, and allow approves it. Exit codes other than 0 and 2 are non-blocking errors. JSON output can also rewrite the tool's arguments via updatedInput or inject guidance via additionalContext.
Where are hooks configured and who controls them?
Hooks live under the hooks key in settings files: ~/.claude/settings.json for the user, .claude/settings.json committed in a project, .claude/settings.local.json ungoverned by git, managed policy settings controlled by an organization, plus hooks bundled by plugins and declared in skill or agent frontmatter. Managed settings win: an organization can ship fleet-wide hooks and use allowManagedHooksOnly to stop user, project, and plugin hooks from running at all.
Are Claude Code hooks a security risk themselves?
Yes, treated carelessly. A hook is arbitrary code that runs with your full user permissions every time its event fires, without a per-run prompt. A malicious hook in a repository's committed .claude/settings.json arrives with a git clone, and a hook added by an injected agent is durable persistence. Hooks deserve the same review as any executable config: know which hooks exist on which machines, where each came from, and when they change. Hooks are one of the eight AI artifact types Anomity inventories for exactly this reason.
How is a hook different from a permission rule or a CLAUDE.md instruction?
A CLAUDE.md instruction is a request to the model - it usually works, and reliably enough for style, but nothing enforces it. Permission rules (allow and deny lists) are enforced but static string patterns. A hook is enforced and programmable: it can inspect the exact tool input, consult external systems, rewrite arguments, and return a decision with a reason. The layers stack well - rules for the easy cases, hooks for policy that needs logic. See how Claude Code permissions work.
How does Anomity relate to Claude Code hooks?
Anomity's Endpoint Sensor uses the agent's hook - Claude Code's PreToolUse - as its enforcement point: each tool call is evaluated against policy and receives allow, deny, or log before it runs, and every decision lands in a queryable 90-day audit trail with violations routed to SIEM, Slack, email, and Jira. The sensor also discovers hooks themselves across the fleet, as one of eight AI artifact types, and surfaces hook and config changes as change events.




