A Claude Code hook is a user-defined handler that runs when a specific lifecycle event occurs. The most common hook is a shell command, but current Claude Code also supports HTTP, MCP-tool, prompt-based and agent-based hooks. Hooks are useful for formatting files, enforcing rules, sending notifications, injecting context, auditing changes and controlling selected tool calls.
if fieldClaude Code has a lifecycle: a session starts, you submit prompts, Claude calls tools, tools finish, Claude responds, the session ends, and so on. A hook lets you attach your own behavior to one of those moments.
The easiest starting point is a Notification hook. It can notify you when Claude
needs your input instead of requiring you to watch the terminal.
For a user-wide hook:
~/.claude/settings.json
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
The example above is for macOS. Linux and Windows use their platform-specific notification commands.
/hooks
The /hooks browser lists hook events and shows configured hooks, including the event,
matcher, type, source file and command.
/hooks browser is read-only. Add, change or remove hooks by editing the settings
JSON or asking Claude to make the change.
Return to the CLI, switch to manual permission mode, ask Claude to do something that requires permission, and switch away from the terminal. The desktop notification should appear.
Hooks fire at specific points in Claude Code's lifecycle. Current Claude Code exposes many events. The most important ones for beginners are:
| Event | When it fires | Typical use |
|---|---|---|
SessionStart | Session begins or resumes | Load environment/context. |
UserPromptSubmit | User submits a prompt | Validate or add context. |
PreToolUse | Before a tool call executes | Block/approve/check a tool call. |
PermissionRequest | A tool call needs permission | Automatically allow selected requests. |
PostToolUse | After a tool call succeeds | Format or log changes. |
PostToolUseFailure | After a tool call fails | Capture failures. |
PostToolBatch | After a parallel tool batch resolves | Process a completed batch. |
Notification | Claude Code sends a notification | Desktop notifications. |
Stop | Claude finishes responding | Validate final state. |
SubagentStart | A subagent is spawned | Track specialized workers. |
SubagentStop | A subagent finishes | Collect/log completion. |
TaskCreated | A task is created | Track task lifecycle. |
TaskCompleted | A task is marked complete | Run completion logic. |
ConfigChange | Configuration changes | Audit or block changes. |
CwdChanged | Working directory changes | Reload environment. |
FileChanged | A watched file changes | React to specific files. |
WorktreeCreate | A worktree is created | Custom worktree setup. |
WorktreeRemove | A worktree is removed | Cleanup. |
PreCompact | Before compaction | Prepare/save context. |
PostCompact | After compaction | Restore context. |
PreModelSwitch | Before model switch | Allow/block a switch. |
PostModelSwitch | After model changes | React to model change. |
Elicitation | MCP asks user for input | Observe/intercept elicitation. |
ElicitationResult | User responds to MCP elicitation | React before response goes back. |
SessionEnd | Session terminates | Cleanup. |
PreToolUse = beforePostToolUse = afterSessionStart = beginningStop = Claude finishedNotification = notify me
Most examples use shell commands, but current Claude Code supports five hook handler types.
| Type | Meaning | Best for |
|---|---|---|
command | Run a shell command. | Scripts, formatting, validation, logging. |
http | POST event data to a URL. | External services/webhooks. |
mcp_tool | Call a tool on an already-connected MCP server. | Use existing MCP integrations. |
prompt | Ask a Claude model to evaluate a condition in a single turn. | Judgment-based checks. |
agent | Use a multi-turn agent with tool access for verification. | More complex verification; experimental. |
.env.”When an event fires, Claude Code passes event-specific JSON to a command hook through stdin.
Common fields include:
session_id — unique session identifier.cwd — working directory when the event fired.hook_event_name — event that triggered the hook.tool_name — tool Claude is using for tool-related events.tool_input — arguments passed to the tool.{
"session_id": "abc123",
"cwd": "/Users/sarah/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}
A Bash hook can therefore inspect exactly what Claude is about to execute.
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
echo "Claude wants to run: $COMMAND"
A command hook communicates with Claude Code through stdin, stdout, stderr and exit codes.
| Exit code | Meaning |
|---|---|
0 | No objection through the exit code. For PreToolUse, this does not itself approve the call; normal permission handling still applies. |
2 | Blocks the action where that event supports blocking. Write the reason to stderr. |
| Other non-zero | Behavior depends on whether the hook emitted valid structured JSON or plain text and on the event. |
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q "drop table"; then
echo "Blocked: dropping tables is not allowed" >&2
exit 2
fi
exit 0
When Claude tries to execute a command containing drop table, the hook blocks the
tool call and provides the stderr message as feedback.
exit 0 with “approve.” For a PreToolUse hook, exit 0 means
the hook itself has no objection; the normal permission system still applies.
Exit codes are useful for simple behavior. Structured JSON gives hooks more precise control. For supported events, print a JSON object to stdout and exit successfully.
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use rg instead of grep for better performance"
}
}
| Decision | Effect |
|---|---|
allow | Skip the interactive permission prompt, subject to other permission/organization rules. |
deny | Cancel the tool call and send the reason to Claude. |
ask | Show the permission prompt normally. |
defer | Available in non-interactive -p mode for external handling. |
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Current branch: release-42. Deploy freeze until Friday."
}
}
UserPromptSubmit, additionalContext belongs inside
hookSpecificOutput. Putting it at the top level is ignored.
A matcher narrows a hook to specific event conditions. Without a matcher, a hook runs every time that event occurs.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
The hook runs after Claude's Edit or Write tool, but not after unrelated
tools.
| Event | Matcher filters | Examples |
|---|---|---|
| Tool events | Tool name | Bash, Edit|Write, mcp__.* |
| SessionStart | Session source | startup, resume, compact |
| Notification | Notification type | permission_prompt, idle_prompt |
| SubagentStart/Stop | Agent type | general-purpose, Explore |
| ConfigChange | Configuration source | user_settings, project_settings |
| FileChanged | Literal filename | .envrc|.env |
| SessionEnd | End reason | clear, resume |
Since Claude Code v2.1.191, comma-separated alternatives also work in the same way as pipe
alternation for supported matchers, for example "Edit, Write".
if Field — Filter by Tool AND Arguments
The matcher filters at the hook-group level by tool name. The if field
can go further by using permission-rule syntax against both the tool and its arguments.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
}
]
}
]
}
}
This targets Git-related Bash commands rather than every Bash call.
| Pattern | Example | Meaning |
|---|---|---|
Bash(git *) | git push | Matches Git commands. |
Edit(*.ts) | TypeScript edit | Matches edits to TypeScript files. |
Bash(git push *) | git push origin main | Targets push commands. |
if filter is best-effort when shell syntax is difficult to analyze. For hard security
allow/deny rules, use the permission system rather than depending on a hook's if filter alone.
| Location | Scope | Shareable? |
|---|---|---|
~/.claude/settings.json | All your projects | No — local machine configuration. |
.claude/settings.json | One project | Yes — can be committed. |
.claude/settings.local.json | One project | No — local/gitignored. |
| Managed policy settings | Organization-wide | Yes — admin controlled. |
Plugin hooks/hooks.json | When plugin is enabled | Yes — bundled with plugin. |
| Skill frontmatter | Rest of session after Skill invocation | Yes — defined in Skill. |
| Subagent frontmatter | While subagent runs | Yes — defined in subagent. |
~/.claude/settings.json..claude/settings.json..claude/settings.local.json.hooks/hooks.json.Useful when Claude takes time and you want to work elsewhere.
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
}
]
}
]
}
}
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
The current guide recommends jq for parsing JSON in these Bash examples.
Create:
.claude/hooks/protect-files.sh
#!/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
Make it executable:
chmod +x .claude/hooks/protect-files.sh
Register it:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing. Current sprint: auth refactor.'"
}
]
}
]
}
}
The hook's stdout is added as plain text to Claude's context for supported session-start behavior.
{
"hooks": {
"ConfigChange": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "jq -c '{timestamp: now | todate, source: .source, file: .file_path}' >> ~/claude-config-audit.log"
}
]
}
]
}
}
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
}
]
}
],
"CwdChanged": [
{
"hooks": [
{
"type": "command",
"command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
}
]
}
]
}
}
This pattern is useful when environment variables depend on the directory Claude is working in.
{
"hooks": {
"PermissionRequest": [
{
"matcher": "ExitPlanMode",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
}
]
}
]
}
}
.* can accidentally auto-approve many tool permission prompts, including writes and shell commands.
Not every rule is deterministic. Sometimes the hook needs a model to evaluate a condition. Prompt-based hooks perform a single-turn LLM evaluation.
Conceptually, use a prompt hook for questions like:
Does this change introduce a security vulnerability?
Return a block decision only if the evidence is strong.
| Command hook | Prompt hook |
|---|---|
| Deterministic script. | Model evaluates context. |
| Good for exact rules. | Good for judgment. |
| Fast/predictable. | Uses model reasoning and can be less deterministic. |
Agent hooks are designed for more involved verification where the evaluator needs multiple turns and tool access.
The current guide marks agent-based hooks as experimental, so behavior may change. Use them when the extra verification capability is worth that tradeoff.
An HTTP hook sends event data to a URL with an HTTP POST request. This is useful when your automation lives outside the local machine.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "http",
"url": "https://example.com/claude-hook"
}
]
}
]
}
}
Typical architecture:
An mcp_tool hook calls a tool on an already-connected MCP server.
This lets hook automation use existing MCP capabilities rather than creating another shell script.
For tool matchers, MCP tools follow the naming pattern:
mcp__<server>__<tool>
For example:
mcp__github__search_repositories
mcp__filesystem__read_file
Plugin-bundled MCP servers can have a scoped server segment, such as:
mcp__plugin_my-plugin_db__query
When multiple hooks match the same event, Claude Code runs matching hooks in parallel and waits for them before merging the results.
One hook returning deny does not stop sibling hooks from executing.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r .tool_input.command >> ~/.claude/bash.log"
},
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh"
}
]
}
]
}
}
The first hook logs the command. The second can block dangerous commands. Even if the second denies the call, the logging hook still ran.
For PreToolUse permission decisions, the most restrictive applicable result wins.
Hooks can run automatically and can block or allow operations. That makes them powerful. Treat hook configuration as code.
| Situation | What to remember |
|---|---|
| Hook modifies a file | Be aware of recursive triggers and unintended side effects. |
| Shell command is complex | The if matcher is best-effort; don't treat it as a security boundary. |
| Hook needs judgment | Consider prompt/agent hooks. |
| Hook needs external service | Use HTTP or MCP when appropriate. |
| Hook is project-specific | Prefer project settings. |
| Hook should be reusable | Package it in a plugin. |
| Hook output is malformed JSON | Claude Code can report a non-blocking hook error. |
| Executable script on macOS/Linux | Make the script executable with chmod +x. |
/hooks and confirm it is registered.claude --debug or /debug.jq is installed.PostToolUse.Edit|Write..tool_input.file_path exists for the event.chmod +x.Edit|Write.2.
Check the settings file location and whether the hook is valid JSON. Use the /hooks
browser and debug logging to inspect configuration loading.
Suppose your Spring Boot team wants Claude to run formatting and validation after Java edits. A project-level hook can automate part of that workflow.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); case \"$FILE\" in *.java) ./mvnw spotless:apply ;; esac"
}
]
}
]
}
}
Conceptually:
A Next.js project can automatically run Prettier after Claude edits frontend files:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(jq -r '.tool_input.file_path // empty'); case \"$FILE\" in *.ts|*.tsx|*.js|*.jsx|*.css) npx prettier --write \"$FILE\" ;; esac"
}
]
}
]
}
}
PreToolUse → inspect → block risky operation.PostToolUse → format changed file.Notification → desktop alert.SessionStart → inject environment/project context.ConfigChange / tool events → write an audit record.CwdChanged → reload environment variables.SessionEnd → remove temporary resources.PreModelSwitch → allow or deny a model switch.SubagentStart/Stop → observe worker activity.| Feature | Best mental model | When to use |
|---|---|---|
| Hook | “When X happens, automatically do Y.” | Deterministic lifecycle automation. |
| Skill | “Claude should know this workflow.” | Reusable instructions and commands. |
| Plugin | “Package my extensions.” | Share Skills, agents, hooks, MCP/LSP and other capabilities. |
| Permission system | “Is this operation allowed?” | Hard access-control rules. |
| Prompt/Agent hook | “Ask a model whether this is acceptable.” | Judgment-based verification. |
if filter.| Goal | Use |
|---|---|
| Browse configured hooks | /hooks |
| Debug hook problems | claude --debug or /debug |
| Before a tool | PreToolUse |
| After a successful tool | PostToolUse |
| When Claude needs attention | Notification |
| Start/resume | SessionStart |
| After compaction | SessionStart with compact matcher |
| Protect files | PreToolUse + Edit|Write |
| Format edits | PostToolUse + Edit|Write |
| Audit settings | ConfigChange |
| Reload env on directory changes | CwdChanged |
| React to specific files | FileChanged |
| Clean up at session end | SessionEnd |
| Call external endpoint | type: "http" |
| Call connected MCP tool | type: "mcp_tool" |
| Use model judgment | type: "prompt" or "agent" |
PreToolUse?PostToolUse?0 mean?2 mean?if field add beyond a matcher?.env from edits?if filter be treated as a hard security boundary?Create a Notification hook that alerts you when Claude needs input.
Create a PostToolUse hook that formats JavaScript/TypeScript after Edit or Write.
Block edits to .env, .git/ and another sensitive file.
Inject the current Git branch after session compaction.
Log every Bash command executed by Claude during a project session.
Auto-approve only one narrowly matched permission request and verify that unrelated requests still prompt.
Send a harmless PostToolUse event to a local test endpoint and inspect the payload.
Move your project's hook into a plugin's hooks/hooks.json and test it.
Hook = “When this event happens, automatically run this behavior.”
PreToolUse is your main guardrail point.
PostToolUse is your main formatting/logging point.
SessionStart is your main context/environment point.
Notification is your main “tell me Claude needs attention” point.
Prompt/agent hooks are for situations where simple deterministic scripts are not enough.
Claude Code hooks are one of the most useful ways to make an AI-assisted development workflow predictable. Instead of hoping Claude remembers to format a file, block a dangerous command, load an environment variable or notify you, a hook can make that behavior happen automatically at the right lifecycle event.
The strongest approach is to keep hooks small, narrow and deterministic. Use matchers to reduce unnecessary executions, use structured JSON when you need precise decisions, and use the permission system for hard security boundaries. When the automation becomes reusable across projects, package it as a plugin.