Quick answer
Claude Code hooks are shell commands defined in settings.json that run automatically at lifecycle events — session start, prompt submit, before a tool runs, on stop, and more. Each event passes context like the session ID and working directory to your command, so you can trigger notifications, logging, or guardrails.
Claude Code hooks are one of the most underused features in the CLI. They let you run custom commands whenever Claude Code does something — starts a session, asks for permission, finishes a task, or uses a tool.
Think of them as webhooks for your terminal. Every time Claude Code hits a lifecycle event, your hook fires. That opens up notifications, logging, guardrails, custom automations — anything you can do with a shell command.
What Are Hooks?
Hooks are shell commands that Claude Code runs at specific points in its lifecycle. You define them in your ~/.claude/settings.json file.
When an event fires, Claude Code executes your hook command synchronously. The hook receives context about what triggered it — session ID, working directory, tool name, and more — either as environment variables or via JSON on stdin.
Because a hook is just a shell command, there's no SDK to learn — if you can write a one-liner in your terminal, you can write a hook by hand. Higher-level integrations can also package these hooks for you: the Agentfy plugin, for instance, installs them through Claude Code's plugin system so you never touch settings.json. Either way, hooks are the lowest-level extension surface Claude Code exposes, and almost every higher-level integration — including phone notifications — is built on top of them.
Available Hook Events
Claude Code supports these hook events:
SessionStart
Fires when a new Claude Code session begins. Use it to initialize logging, send a "session started" notification, or set environment variables for the session.
UserPromptSubmit
Fires when you send a message to Claude. Useful for logging prompts or triggering external workflows based on what you're asking Claude to do.
PreToolUse
Fires before Claude executes a tool (Bash, Edit, Read, etc.). This is powerful for guardrails — you can inspect what Claude is about to do and block it by returning a non-zero exit code. For example, you could prevent rm -rf / from ever running.
Stop
Fires when Claude stops and is waiting for your input. This is the most common hook for notifications — it tells you "Claude is done and needs you."
Notification
Fires when Claude explicitly sends a notification to you. This is separate from Stop — it's for custom messages Claude wants you to see, like "Build failed" or "All tests passing."
PermissionRequest
Fires when Claude needs your permission to do something — run a command, edit a file, make an API call. The hook receives the tool name and details about what Claude wants to do.
SessionEnd
Fires when a session ends. Use it for cleanup, final logging, or sending a "session ended" notification.
For monitoring, the two events that matter most are Stop (Claude finished its turn and is waiting on you) and Notification (Claude is surfacing a specific message). Together they let a tool distinguish "still working," "needs a decision," and "done" without you watching the terminal. See how to get Claude Code notifications on iPhone for how these map to real alerts.
How to Configure Hooks
If your goal is phone notifications, you don't have to hand-write any of this — the open-source Agentfy plugin installs the hooks for you. The manual configuration below is still worth understanding, and it's the right tool for custom automations, guardrails, and logging.
Hooks live in ~/.claude/settings.json. Here's the basic structure:
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "your-command-here"
}
]
}
]
}
}
Each hook event takes an array of hook groups. Each group has:
- matcher — a pattern to filter when this hook runs. An empty string means "always run." For
PreToolUse, you can match on tool names likeBashorEdit. - hooks — an array of commands to run when the event fires.
- type — currently only
"command"is supported. - command — the shell command to execute.
Environment Variables in Hooks
Claude Code exposes context as environment variables that your hook commands can use:
| Variable | Description |
|---|---|
$SESSION_ID | Unique identifier for the current session |
$CWD | Current working directory |
$CLAUDE_SESSION_ID | Same as $SESSION_ID (v2.1.9+) |
$CLAUDE_WORKING_DIR | Same as $CWD (v2.1.9+) |
$CLAUDE_TOOL_NAME | Name of the tool being used (in tool hooks) |
In addition to environment variables, Claude Code also passes a JSON payload on stdin for each event. If you need richer context — the full tool input, the message body, or fields that aren't exposed as env vars — read stdin and parse it with a tool like jq. Environment variables are the quickest path for simple commands; stdin is there when you need everything.
Practical Examples
1. Desktop Notification When Claude Stops
The simplest useful hook — a macOS notification when Claude is done:
{
"hooks": {
"Stop": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "osascript -e 'display notification \"Claude needs you\" with title \"Claude Code\"'"
}]
}]
}
}
2. Push Notifications to Your iPhone
Send a webhook to a notification service. Agentfy uses this pattern — you point your hooks at its webhook URL and get push notifications, Live Activities, and Dynamic Island updates on your iPhone:
{
"hooks": {
"Stop": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "curl -s -X POST https://your-webhook-url -H 'Authorization: Bearer YOUR_TOKEN' -H 'Content-Type: application/json' -d '{\"hook_event_name\": \"Stop\", \"session_id\": \"$SESSION_ID\", \"cwd\": \"$CWD\"}'"
}]
}],
"SessionStart": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "curl -s -X POST https://your-webhook-url -H 'Authorization: Bearer YOUR_TOKEN' -H 'Content-Type: application/json' -d '{\"hook_event_name\": \"SessionStart\", \"session_id\": \"$SESSION_ID\", \"cwd\": \"$CWD\"}'"
}]
}]
}
}
Writing webhook hooks like this by hand is fiddly — quoting, headers, tokens, and remembering to wire every event. In practice you don't have to. The easiest path is the open-source Agentfy plugin:
claude plugin marketplace add ibrillantes/claude-code-plugin
claude plugin install agentfy@agentfy --config api_token=<YOUR_TOKEN>
# then, inside Claude Code:
/reload-plugins
The plugin collects your token in a config dialog and stores it in your OS keychain — not a plaintext file — then wires up every event for you. Because it's open source, you can read exactly what it sends. On older Claude Code without plugin support, you can still paste Agentfy's ready-made setup prompt and it writes the hooks itself. Either way, you skip the hand-editing.
Stop babysitting your terminal
Agentfy pushes Claude Code status to your iPhone — Live Activities, Dynamic Island, and instant alerts.
3. Block Dangerous Commands
Use PreToolUse to prevent Claude from running specific commands. If your hook exits with code 2, the tool use is blocked:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "echo $CLAUDE_TOOL_INPUT | grep -q 'rm -rf /' && exit 2 || exit 0"
}]
}]
}
}
A guardrail hook is a safety net, not a security boundary. A single grep check only catches the exact pattern you wrote — it won't stop creative variations of a destructive command. Treat PreToolUse blocks as defense-in-depth alongside running Claude with appropriate permissions, not as your only line of defense.
4. Log All Sessions
Append session events to a log file for tracking your Claude Code usage:
{
"hooks": {
"SessionStart": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "echo \"$(date): Session $SESSION_ID started in $CWD\" >> ~/.claude/session.log"
}]
}],
"SessionEnd": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "echo \"$(date): Session $SESSION_ID ended\" >> ~/.claude/session.log"
}]
}]
}
}
Multiple Hooks Per Event
You can chain multiple hooks on the same event. They run in order. For example, you might log to a file and send a push notification on every Stop event. Each hook in the array executes independently — if one fails, the others still run.
This is what makes hooks scale across a real workflow: a single Stop event can log usage, ping a teammate, and update a phone monitor at once. It's also why running several Claude Code agents in parallel works cleanly — every terminal's Stop and Notification events route to the same place, so you watch one screen instead of tabbing between sessions.
Project-Level vs Global Hooks
Hooks in ~/.claude/settings.json apply to all your Claude Code sessions. You can also add project-specific hooks in a .claude/settings.json file at the root of your project.
Project-level hooks run in addition to global hooks, not instead of them. This is useful for project-specific guardrails — for example, blocking database drops in production projects.
Tips and Gotchas
- Hooks run synchronously. A slow hook blocks Claude Code. Keep them fast — use
curl -s(silent mode) and avoid commands that take more than a second or two. - After editing
settings.json, restart Claude Code. Use/resumeto continue your conversation without losing context. - Test hooks manually first. Copy the command and run it in your terminal to make sure it works before adding it to settings.json.
- Use
-swith curl. Without it, curl outputs progress bars that can clutter Claude Code's output. - Session IDs change on resume. If you use
--resume, the new session gets a new ID. Don't rely on session IDs being stable across resumes.
If a hook isn't firing — no notification, no log line — check three things in order: did you restart Claude Code after editing settings.json, does the command run on its own in your terminal, and is the JSON valid (a stray comma will silently disable the whole hooks block). For the phone-notification case specifically, this troubleshooting guide walks through the usual culprits.
What's Next for Hooks
Hooks are still evolving. The current version supports shell commands, but the Claude Code team is actively expanding what's possible. If you want to stay on top of new hook events and capabilities, check the official hooks documentation.
In the meantime, even the basic hooks above can save you hours of terminal babysitting. Set up notifications, add some guardrails, and let Claude Code work while you don't. If you mostly want the notification payoff without hand-writing webhook configs, getting Claude Code on your iPhone is the fastest way to put these events to work.