Deep Diveclaude code hooksclaude code webhooksclaude code automation

Claude Code Hooks: A Complete Guide to Automating Your Workflow

Everything you need to know about Claude Code hooks — what they are, how they work, and how to use them for notifications, logging, and custom automations.

Ian Brillantes · Founder & iOS EngineerFebruary 20, 20269 min read

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.

Info

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

Tip

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 like Bash or Edit.
  • 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:

VariableDescription
$SESSION_IDUnique identifier for the current session
$CWDCurrent working directory
$CLAUDE_SESSION_IDSame as $SESSION_ID (v2.1.9+)
$CLAUDE_WORKING_DIRSame as $CWD (v2.1.9+)
$CLAUDE_TOOL_NAMEName 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.

Download Agentfy

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"
      }]
    }]
  }
}
Heads up

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 /resume to 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 -s with 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.
Tip

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.

About the author

Ian Brillantes · Founder & iOS Engineer

Ian is the founder of Agentfy and a senior software engineer who lives in Claude Code daily. He builds the hooks-to-push-notification pipeline behind Agentfy and writes these guides from the same multi-agent workflow they describe.

Part of Running Claude Code Like a Pro

More guides in this series

Frequently asked questions

What hook events does Claude Code support?+
Claude Code fires hooks on SessionStart, UserPromptSubmit, PreToolUse, Stop, Notification, PermissionRequest, and SessionEnd. Each maps to a point in the session lifecycle — beginning, sending a prompt, before a tool runs, finishing a turn, a custom message, a permission ask, and ending. You attach a shell command to any of them in settings.json.
Where is the Claude Code settings.json file?+
Global hooks live in ~/.claude/settings.json on your machine and apply to every session. You can also add project-specific hooks in a .claude/settings.json file at the root of a project. Project-level hooks run in addition to global ones, not instead of them, so both sets fire when an event matches.
How do I send a notification from a Claude Code hook?+
Attach a command to the Stop event. On macOS, use osascript to fire a desktop notification when Claude finishes. For your phone, the easiest path is the open-source Agentfy plugin, which installs the hooks for you; or point a hook at a webhook URL with curl -s -X POST manually and let Agentfy turn that event into a push notification, Live Activity, and Dynamic Island update on iOS.
How do I block a dangerous command with a hook?+
Use a PreToolUse hook with a matcher like Bash. Inspect the tool input and exit with code 2 to block the action — for example, grep the input for rm -rf / and exit 2 if it matches, otherwise exit 0. A non-zero exit stops Claude from running that specific tool call before it executes.
Do I need to restart Claude Code after editing hooks?+
Yes, if you edit settings.json by hand — changes are read when a session starts, so restart Claude Code after editing your hooks and use /resume to continue the same conversation without losing context. If you manage hooks through a plugin like Agentfy, run /reload-plugins instead of restarting. It's also worth testing each hook command directly in your terminal first to confirm it works before relying on it inside a session.

Related articles

Ready to stop babysitting your terminal?

Agentfy pushes Claude Code status to your iPhone — Live Activities, Dynamic Island, and instant alerts. Set up in under a minute.

Download Agentfy