Claude Code hooks are shell commands that Claude Code runs at fixed points in its lifecycle: when a session starts, before or after a tool call, when it needs a permission, or when Claude finishes. The difference from a rule in your CLAUDE.md is how binding it is. A rule in CLAUDE.md is something Claude reads and usually follows. A hook runs every time, because the decision is made by Claude Code itself, not by the model. You add it as JSON to a settings file, and from then on it applies in every session.
I've used Claude Code daily since it came out, and I run my business with an AI workforce built on Claude. Every employee in it works in a terminal, and every permission one of them needs passes through a hook first. Everything I write about Claude is collected on the Claude page; if you're new to Claude Code, start with Claude Code: A Guide for Beginners.
Claude Code hooks explained: why a CLAUDE.md rule isn't enough
Anthropic's hooks guide puts it this way: "Hooks are user-defined shell commands. Claude Code runs them at specific points in its lifecycle, which gives you deterministic control: certain actions always happen rather than relying on the LLM to choose to run them." The key word is "deterministic". The best practices add: "Unlike CLAUDE.md instructions which are advisory, hooks are deterministic and guarantee the action happens."
The weight sits in that word "advisory": CLAUDE.md goes to the model, and the model decides how to apply it; the docs promise no guarantee for any given session. What you put in CLAUDE.md: Claude Code's Memory is context, not law. A hook is law. Anthropic states it in one line: "Use hooks for actions that must happen every time with zero exceptions."
My rule of thumb: anything Claude should know goes in CLAUDE.md. Anything that must happen without exception goes in a hook. And anything Claude should never be allowed to do goes in the permission rules of your settings. The three layers side by side:
How a hook works: event, matcher, command, exit code
According to the hooks reference, a hook has four parts. The event says when it runs, for example PreToolUse before every tool call. The matcher narrows down which tools: Bash, Edit|Write, or a regular expression like mcp__.*; an empty matcher or * means all of them. The command is a shell command or a script. And the command's exit code tells Claude Code what happens next.
Exit codes are the part to remember. Exit 0 means success: whatever the script writes to stdout is read by Claude Code as JSON, and for UserPromptSubmit and SessionStart plain text is passed to Claude as context. Exit 2 means a blocking error: for PreToolUse the tool call is skipped, for UserPromptSubmit the input is discarded, for Stop Claude keeps working. Whatever the script writes to stderr reaches Claude as the reason. Any other exit code doesn't block; if stdout has valid JSON, the JSON decides.
The script gets its input as JSON on stdin: among other fields session_id, cwd, hook_event_name, and for tool events tool_name and tool_input; the full list per event is in the reference. Claude Code also sets CLAUDE_PROJECT_DIR so your script knows the project folder.
Where the hook lives decides its reach: ~/.claude/settings.json applies to all your projects, .claude/settings.json inside a project is shared via Git, .claude/settings.local.json stays local. Hooks from plugins and from the frontmatter of skills and subagents are merged on top. /hooks only displays them, you create them in the JSON file. Anthropic recommends letting Claude write the hook, for example with "Write a hook that blocks writes to the migrations folder".
The seven events you need day to day
As of September 2026, the reference lists more than thirty events. Seven are enough to begin with:
SessionStart runs on startup, on resume, and after compaction.
UserPromptSubmit runs on every input, with no matcher. Use it to check or enrich your input.
PreToolUse runs before every tool call and is the event that lets you really protect files.
PostToolUse runs after the tool. Blocking is no longer possible, checking is.
PermissionRequest runs when Claude Code needs a permission. Exit 2 does not block here according to the reference; the decision comes back as JSON. This is the event my workforce is built on.
Stop runs when Claude is done. Exit 2 sends it back to work.
Notification runs when Claude Code needs you, for example on permission_prompt or idle_prompt.
The reference also lists the hook types http, mcp_tool, prompt, and agent; command stays the clearest choice, because you can test the script without Claude.
Claude Code hooks tutorial: your first hook from the official docs
The example I recommend to beginners comes straight from Anthropic's hooks guide: protection for files Claude must not touch. The script lives at .claude/hooks/protect-files.sh and has to be executable via chmod +x:
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
FILE_PATH="${FILE_PATH//\\//}"
PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done
exit 0
The script reads the JSON input, pulls out the file path with jq, and compares it against a list of protected patterns. On a match it writes the reason to stderr and exits with code 2, and Claude Code aborts the write. The script is wired up in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
The matcher Edit|Write limits the hook to writes, not every Read or Bash. To test it, ask Claude to add a line to .env; the reply has to mention the block. The second example from the same guide fixes a familiar problem: after compaction, your context rules are gone. A SessionStart hook with the matcher compact restores them:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing.'"
}
]
}
]
}
}
Whatever the echo prints reaches Claude as context on exit 0. That's the simplest form a hook can take: one line of text that is reliably back after every compaction.
A hook from my workforce: permissions land in the employee OS
My AI employees run as Claude Code sessions in the terminal, each in its own folder. The problem I solved with hooks is practical: I'm not sitting in every terminal. Conny and Ralf regularly run in long sessions, and when one of them needs a permission, I want to see it in one place and decide there.
For that, my ~/.claude/settings.json (as of September 2026) hooks into six events: SessionStart, UserPromptSubmit, Stop, PermissionRequest, SessionEnd, and PostToolUse. All six call the same shell script, mos-hook.sh, with no matcher, so it applies to every tool. The script reports each event via HTTP POST to my employee OS, a local service on port 3210 at /api/hooks. One interface then shows me which employee is starting, working, finished, or waiting for me.
The most important case is PermissionRequest. When an employee asks for a permission, the script waits up to 25 seconds for an answer from the OS (settings.json sets 30 as the limit). If I decide within that time, the service replies with hookSpecificOutput, and the script hands that decision back to Claude Code. Otherwise the normal permission prompt appears in the terminal. For all other events the timeout is 2 seconds, so no employee waits on my OS.
Two decisions in the script mattered. First, it always exits with code 0, whether the OS is reachable or not: a hook that blocks work because a helper service is down would be worse than no hook. Second, it recognizes runs the OS itself started by their environment variables (PASEO_AGENT_ID, MOS_RUN_ID) and skips them; otherwise OS and hook would pass events back and forth. I verified in September 2026 that this works live and documented it in the repo. The approval itself is mine to give; the employee in question records it with a timestamp.
The other direction matters just as much: what is deliberately not a hook here. Secret scanning, for example. I was convinced gitleaks ran automatically before every commit. When Sebastian, my SEO employee, checked in September 2026, none of it was there: no Git hook, no core.hooksPath, no husky in any repository he checked. What does exist is gitleaks installed through Homebrew and one line in my workforce's house rules: run gitleaks git . --staged before every push. That is a rule for the employees, not an automation. In one customer project gitleaks additionally runs as a CI job, so on the server after the push, not locally before it.
The lesson fits the rule of thumb above. A hook is for what has to happen at a fixed point in Claude Code's lifecycle and can be decided there in seconds. A rule is for what happens outside a session or takes longer than a hook may wait: a push does not necessarily happen inside a Claude Code session, and a repository-wide scan is not a two-second job. Rule and CI job together cover more than a hook alone. If you believe something runs automatically, check whether it is written in a file.
I don't start tests or linters through hooks either; my employees write texts, audits, and configuration. If you develop software, the examples in the docs are your starting point.
Claude Code hooks on Windows: PowerShell instead of Bash
On Windows, according to the reference, shell-form hooks run in Git Bash, and in PowerShell without it. Calls through .cmd or .bat files need the args form, with command and arguments listed separately. The official pattern for a PowerShell script looks like this: "command": "powershell.exe" with "args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.ps1"]. The script reads stdin with [Console]::In.ReadToEnd() | ConvertFrom-Json and answers with ConvertTo-Json.
Two Windows quirks from the docs: Notification hooks may surface their message as a dialog box instead of a corner notification. And shell profiles that echo something on every start put that text in front of your hook's JSON, which Claude Code then ignores. If a hook on Windows seems to do nothing, check the profile first.
Limits, errors, and security
Hooks can tighten rules, but not loosen them. The guide says so literally: "Hooks can tighten restrictions but not loosen them." A deny from a PreToolUse hook always applies, even if you start Claude Code with --dangerously-skip-permissions; an allow from a hook does not override a deny rule.
The second point is the stop cap. A Stop hook that keeps sending Claude back can build an endless loop. According to the docs, Claude Code overrides it after eight blocks without progress; your script should check stop_hook_active and exit 0 when it's true. The third point is timeouts: up to 600 seconds per command hook, 30 seconds for UserPromptSubmit. My script uses 2, because every second in a hook is a second an employee waits.
Finally, security: a hook is code that runs with your permissions, on every event, without asking. A hook in a shared .claude/settings.json applies to everyone who starts Claude Code in that repository, and it is not gated by the folder's trust dialog, unlike a project's permissions.allow rules (Anthropic's permissions docs). So read other people's hooks before you start in a project you did not write; for one run, --settings '{"disableAllHooks": true}' turns them off.
Frequently asked questions
What's the difference between a hook and a rule in CLAUDE.md?
A rule in CLAUDE.md is an instruction to the model; Claude reads it and decides how to apply it. A hook is a command Claude Code itself runs, whatever the model is planning. Anthropic calls the first "advisory" and the second "deterministic".
Can Claude simply bypass a hook?
No, a hook runs outside the model. According to the guide, PreToolUse hooks run before every permission check, and a deny always applies. What Claude can do is reach the goal with a different tool, if your matcher only checks Edit|Write and a Bash call changes the same file.
Where do I put a hook so it applies to all my projects?
In ~/.claude/settings.json. That's exactly where my workforce hook lives, because it has to apply in every employee folder. According to the reference, cloud sessions of Claude Code don't read this file; there you need the project settings.
Do Claude Code hooks work on Windows without Git Bash?
Yes, according to the reference they run in PowerShell. For scripts, use the args form with powershell.exe and the switches -NoProfile, -ExecutionPolicy Bypass, and -File.
Why doesn't my hook fire even though it's in settings.json?
The most common causes: a script without execute permission, a misspelled event name, invalid JSON in the settings file, or text your shell profile prints before the JSON output. Check with /hooks whether Claude Code knows the hook, and run the script once by hand.
Where can I find more Claude Code hooks examples?
In Anthropic's hooks guide, which also covers formatters, notifications, and context after compaction. The fastest route, though, is to ask Claude itself: describe what must always happen and let it write the script and the JSON.
Where to go next
Once your hooks are in place, the next step is splitting up the work: how subagents, skills, and agents fit together is in Claude Subagents, Skills, Agents, and what matters in practice is in Claude Code Subagents: Best Practices. Recurring workflows go into Claude Skills: How to Create and Use Them; if a hook needs external services, read Set Up a Claude MCP Server first.
The eight ready-made AI employee packages, each with its own personnel file and skills, are available in my community. My suggestion for getting started: take the file protection hook from the docs, add your three most important files, and ask Claude to change one. When the block shows up, you've understood what a hook is.