Guide
How do Claude Code hooks work, and what can they enforce for security?
Updated
Short answer
Claude Code hooks are shell commands, HTTP endpoints, MCP tool calls, LLM prompts or subagents that Claude Code runs automatically at fixed points in a session. A PreToolUse hook sees each tool call before it runs and can block it: exit code 2 stops the call, while exit code 1 is a non-blocking error and the call goes ahead. A hook is only as strong as the check behind it. On most events it fails open unless you set it to fail closed.
How hooks work#
Anthropic describes hooks as "user-defined shell commands, HTTP endpoints, MCP tool calls, LLM prompts, or subagents that execute automatically at specific points in Claude Code's lifecycle." Claude Code runs them. The model doesn't get to choose whether they run.
That's why security teams like them. In Anthropic's words, hooks give you "deterministic control: certain actions always happen rather than relying on the LLM to choose to run them." No tired human has to click through a prompt.
The events that matter for security#
The hooks reference lists every event and its input schema. Check it before you build anything, because the schema changes. These are the ones a security team cares about most:
- PreToolUse: fires before a tool call executes. This is the one that can block.
- PostToolUse: fires after the tool already ran. Use it for logging, not prevention.
- UserPromptSubmit: fires when a prompt is submitted, before Claude processes it.
- ConfigChange: fires when a configuration file changes during a session.
- SessionStart / SessionEnd: fire at the start and end of a session, which is useful for audit.
A command hook gets the event as JSON on stdin. For tool events that includes the tool name and its input, like the Bash command or the file path.
Exit codes: 2 blocks, 1 doesn't#
This is the detail that breaks the most homemade policies. Exit code 2 is the blocking error. Exit code 1, the normal Unix failure code, is not.
Anthropic's docs: "Exit code 1 without valid JSON output is a non-blocking error unless the hook is one whose failure blocks the action, even though 1 is the conventional Unix failure code. If your hook is meant to enforce a policy, use exit 2."
For finer control, a PreToolUse hook can exit 0 and print JSON. Inside hookSpecificOutput, a permissionDecision of allow, deny or ask sets the outcome, and a permissionDecisionReason tells the agent why.
A PreToolUse example, and how it breaks#
Picture a small bash script that reads the event JSON from stdin with jq. It pulls out the tool name, the Bash command and the file path. If the path looks like a .env file, or the command pairs cat, grep or head with .env, it writes a reason to stderr and exits 2. If the Bash command matches rm -rf, same thing. Otherwise it exits 0.
You register it under hooks.PreToolUse in settings, with a matcher of Bash|Read|Edit|Write and a hook of type command that points at the script. Treat it as a starting point, not a boundary.
Now try to get around it. rm -fr, find . -delete, a Python one-liner and cat $(echo .e*) all walk straight past this script, because it matches text. Every string-matching hook has the same hole. More on that in why deny-lists catch rm -rf as typed.
Where hooks live#
| Location | Scope | Who can change it |
|---|---|---|
~/.claude/settings.json | All of one user's projects | The user, and the agent if it can write there |
.claude/settings.json | One project, committed | Anyone who can commit to the repo |
.claude/settings.local.json | One project, gitignored | The user |
| Managed policy settings | Organization | Admins |
If a hook is a security control, it belongs in managed settings, pushed by the MDM you already run. Claude Code's allowManagedHooksOnly setting limits hooks to the managed ones, so a developer or an agent can't quietly add their own. Check the current settings docs for the exact behavior before you rely on it.
What hooks can't do#
Know these before you call a hook a control:
- They fail open on most events. Anthropic's docs: "On most events, when a hook fails or times out, Claude Code still carries out the action." To fail closed, set
"onFailure": "block", which needs Claude Code v2.1.295 or later. - They judge one call at a time. A hook sees the input of one tool call. It doesn't know what the session was asked to do unless you build that yourself.
- They run as the user. A hook has the developer's credentials and environment. Project hooks get committed to the repo, so review them like code.
- A writable config is a weak config. If the agent can edit
.claude/settings.jsonor the hook script, it can change the check. Managed settings and read-only hook files close that gap. - Anthropic doesn't recommend them as the hard boundary. The docs: "use the permission system rather than a hook to enforce a hard allow or deny." See permission rules vs hooks vs sandboxing for how the layers stack.
A quick test before you sign off on a hook-based control:
- Does it exit 2, not 1, when it means to block?
- Is
onFailureset to block? - Is it in managed settings, with non-managed hooks turned off?
- Did someone try the four bypasses above and watch what happened?
Where Harden fits#
The hook is the right place to check an action. The seat matters: a check that runs outside the agent can't be edited by the agent.
Harden Agentic Integrity Foundation (AIF) runs on the developer laptop, outside the agent, at the agent's pre-execution hook. Harden checks every command, file edit and push against the session's intent before it runs. Deterministic rules cover 10 kinds of risky action, and a local model reviews 19 risk categories in context. Decisions are made locally in milliseconds, and nothing leaves the machine.
It's a layer, not a perimeter. It doesn't replace least privilege, scoped credentials or egress policy. One policy covers every agent, your MDM pushes it, and it starts watch-only so you can see what agents do before you enforce. See Harden for security teams.
FAQ
Related questions
What is the difference between exit code 1 and exit code 2 in a Claude Code hook?
Exit 2 is the blocking error, so on PreToolUse it stops the tool call. Exit 1 without valid JSON is a non-blocking error on most hooks, so the action goes ahead. Anthropic's docs say to use exit 2 if a hook enforces policy.
Can a developer or the agent change hooks?
Whoever can write the user or project settings files, or the hook script, can change the check, and that includes the agent. Put security hooks in managed settings, make hook files read-only, and restrict hooks to managed ones.
Do Claude Code hooks fail open or closed?
Open, on most events. A hook that fails or times out still lets the action run. Setting onFailure to block makes it fail closed, on Claude Code v2.1.295 or later.
Are hooks a replacement for permission rules or a sandbox?
No. Anthropic's docs recommend the permission system for hard allow or deny rules. Hooks add custom checks, a sandbox limits what the agent can reach, and you want all three.
Can hooks use an LLM to decide?
Yes. Besides shell commands, HTTP endpoints and MCP tool calls, a hook can be an LLM prompt or a subagent. Whatever decides, the exit-code and fail-open rules still apply.