Claude Code hooks: the complete guide
Claude Code hooks let you run your own shell command every time the agent reaches a point in its lifecycle: it starts a session, calls a tool, needs your permission, finishes a turn. They are the official, supported way to react to what Claude Code is doing without polling anything or parsing its output.
This guide covers what hooks are, every event they fire on, how to configure them, what your command receives, how it can talk back, and a few examples you can paste straight into your settings.
What a hook actually is
A hook is a command Claude Code runs when a given event happens. Claude pipes a small JSON payload describing the event to your command on standard input; your command does whatever it wants with it (send a notification, write a log, format a file, block a dangerous call) and exits.
That is the whole model. There is no plugin API and no long-running process to manage: just a command, run at the right moment, with the event on stdin.
Where hooks live
Hooks are declared in a settings.json file, under a top-level hooks key. Claude Code layers several of them, most specific winning:
- Local project settings:
.claude/settings.local.json, your personal overrides for one repository, kept out of git. - Project settings:
.claude/settings.jsonin your repository, shared with your team. - User settings:
~/.claude/settings.json, applied to every project on your machine.
A minimal file looks like this:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "echo done >> ~/claude.log" }
]
}
]
}
}
Each event maps to an array of matchers, and each matcher holds an array of hooks (type: "command" with a command string). For events tied to a tool, the matcher can filter by tool name; for lifecycle events, you leave the matcher out and the hooks always run.
You can also edit all of this from inside Claude Code with the /hooks slash command, which writes the same JSON for you.
Every hook event
Claude Code exposes a hook for each meaningful moment in a session. Here is what each one fires on:
| Event | Fires when |
|---|---|
SessionStart | A session opens (or resumes, or is cleared). |
UserPromptSubmit | You send a prompt and the turn begins. |
PreToolUse | Just before Claude runs a tool (Bash, Edit, Read…). |
PostToolUse | Right after a tool call returns. |
Notification | Claude needs your attention, e.g. to approve a step. |
Stop | Claude finishes its turn and hands control back to you. |
SubagentStop | A spawned subagent (Task) finishes. |
PreCompact | Claude compacts the context on a long run. |
SessionEnd | The session closes. |
Between them, these events let you follow a session from start to finish: idle, working, waiting on you, compacting, done. Each one is detailed in what every Claude Code hook event means.
Filtering by tool
For PreToolUse and PostToolUse, the matcher selects which tools trigger the hook. The matcher is a regular expression tested against the tool name, so you can target one tool or several:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo 'about to run a shell command'" }
]
},
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "echo 'about to touch a file'" }
]
}
]
}
}
mcp__.* matches every tool coming from an MCP server, which is a handy way to audit third-party tooling separately from Claude's built-ins.
What your command receives
Claude writes a JSON object to your command's standard input. Every event carries the same base fields:
| Field | Contains |
|---|---|
session_id | The id of the session that fired the event. |
transcript_path | Path to the session's conversation log. |
cwd | The directory the session is running in. |
hook_event_name | The event name, e.g. Stop. |
Event-specific fields come on top: tool_name and tool_input for tool events, tool_response for PostToolUse, prompt for UserPromptSubmit, message for Notification. The fastest way to see the real shape is to dump it:
{
"hooks": {
"PreToolUse": [
{
"matcher": ".*",
"hooks": [
{ "type": "command", "command": "cat >> ~/claude-events.jsonl" }
]
}
]
}
}
Open that file while you work and you will see exactly what each event carries.
Reading the payload in a real script
Inline one-liners get unreadable fast. Point the hook at a script instead, and use $CLAUDE_PROJECT_DIR so the path works from any subdirectory:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify.sh",
"timeout": 10
}
]
}
]
}
}
#!/usr/bin/env bash
# .claude/hooks/notify.sh
payload=$(cat)
project=$(basename "$(echo "$payload" | jq -r '.cwd')")
notify-send "Claude Code" "$project finished"
timeout is in seconds and caps how long Claude waits for your command (60 by default). Keep hooks fast: they run on the critical path of the session.
How a hook talks back
Exit codes are the simple channel:
0: success. Anything onstdoutis shown in the transcript.2: blocking error.stderris fed back to Claude, and onPreToolUsethe tool call is cancelled.- anything else: non-blocking error.
stderris shown to you and the session carries on.
For finer control, print a JSON object on stdout. On PreToolUse you can decide the permission outright:
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"never touch .env"}}'
A useful example: react when Claude finishes
The Stop event is the one most people want first: it tells you a run is done. On macOS, this posts a notification:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\"'" }
]
}
]
}
}
On Linux, swap in notify-send "Claude Code" "Claude finished".
Debugging a hook that does nothing
Two things catch everyone out. First, Claude Code snapshots your hooks when a session starts, so a hook you add mid-session will not fire until you restart or review it from /hooks. Second, hooks run without your interactive shell setup, so a command that works in your terminal can fail here on a missing PATH entry. Run claude --debug to see each hook being matched, executed and its exit code.
A word on safety
Hooks run arbitrary commands on your machine, with your permissions, automatically. Only add hooks you understand, keep project-level hooks in review like any other code, and be wary of copying a settings.json you did not read. A hook is as powerful as anything you would type in your own shell.
Let a mascot read the hooks for you
Wiring hooks by hand is great for one-off scripts, but if all you want is to see what every session is doing, that is exactly what Blooby does. It sets up its own Claude Code hooks for you, with no settings.json editing, and turns every event above into a live desktop mascot: idle when waiting, busy while working, looking up the moment it needs permission, celebrating when a run finishes.
One session or five, you read the room at a glance instead of tailing a log.
See it for yourself
download Get Blooby free