Claude Code Hooks: Every Event and the Hooks Real Plugins Ship

Mert DüzgünPublished 7 minute read

Claude Code hooks are commands that Claude Code runs at fixed points in a session: before a tool call, after a file edit, when a session starts, when Claude stops. On November 14, 2026 we scanned the 2,195 most-starred GitHub repos behind CCM listings (out of 6,321) and found 1,076 hook config files in 477 of them. The numbers below come from that scan.

What Claude Code hooks are

The hooks guide calls hooks "user-defined shell commands" that give you "deterministic control: certain actions always happen rather than relying on the LLM to choose to run them."

Command hooks read the event as JSON on stdin and answer with an exit code and, if they want, JSON on stdout.

Where hooks live

Per the hooks reference, you can define hooks in:

  • ~/.claude/settings.json, for all your projects
  • .claude/settings.json, for one project (you can commit it)
  • .claude/settings.local.json, for one project, gitignored when Claude Code writes to it
  • managed policy settings, set by an admin for an organization
  • a plugin's hooks/hooks.json, active while the plugin is enabled
  • skill frontmatter, from when the skill is invoked to the end of the session
  • subagent frontmatter, while that subagent runs

Hooks from all levels merge. To see every configured hook and where it comes from:

  1. Inside Claude Code, type:

    /hooks
    

Two environment variables matter most. CLAUDE_PROJECT_DIR is the project root where the session started. CLAUDE_PLUGIN_ROOT is the plugin's install directory, which plugins use to find their own scripts. In our scan, 1,759 hook commands use ${CLAUDE_PLUGIN_ROOT} and 432 use $CLAUDE_PROJECT_DIR.

Claude Code hooks events list

The reference lists these events (as of November 15, 2026):

  • Session: SessionStart, Setup, SessionEnd, InstructionsLoaded, ConfigChange, CwdChanged, DirectoryAdded, FileChanged
  • Prompts: UserPromptSubmit, UserPromptExpansion, MessageDisplay, Notification
  • Tools: PreToolUse, PermissionRequest, PermissionDenied, PostToolUse, PostToolUseFailure, PostToolBatch
  • Turns and agents: Stop, StopFailure, SubagentStart, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle
  • Context and models: PreCompact, PostCompact, PreModelSwitch, PostModelSwitch
  • Worktrees: WorktreeCreate, WorktreeRemove
  • Model Context Protocol (MCP) input: Elicitation, ElicitationResult

That is 33 events (the grouping is ours).

Matchers

A matcher filters which hooks run for an event. For tool events it matches the tool name. Edit|Write is an exact list. Other characters make it an unanchored JavaScript regex, like mcp__memory__.*. "*", an empty string or no matcher matches everything. SessionStart matches startup, resume, clear, compact or fork. Some events take no matcher at all, including UserPromptSubmit and Stop.

The most common matchers in our scan: PreToolUse on Bash in 143 repos, PostToolUse on Write|Edit (any order, with or without MultiEdit) in 132, PostToolUse on Bash in 59, and SessionStart on startup|resume|clear|compact in 39.

Handler types

The docs list five handler types. command runs a shell command. http posts the event to a URL. mcp_tool calls a tool on a configured MCP server. prompt asks a model for a single-turn judgment. agent runs a multi-turn check with tool access and is marked experimental.

Real repos almost always pick command: 3,597 handlers in our scan, against 16 prompt, 6 mcp_tool and 5 agent. We also counted 19 handlers typed process, a type the hooks docs do not list. Most commands run on node (1,484), bash or sh (638) or python (423).

Exit codes and blocking

Exit 0 means success. On SessionStart, UserPromptSubmit, UserPromptExpansion and PostModelSwitch, plain text on stdout is added to Claude's context.

Exit 2 is a blocking error, and stderr becomes the reason. It blocks on PreToolUse, UserPromptSubmit, Stop, SubagentStop, PreCompact and several more. It does not block on PostToolUse, SessionStart or Notification. Any other exit code without valid JSON is a non-blocking error: the action goes ahead and the transcript shows a hook error notice.

For PreToolUse you can also exit 0 and print a decision:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Destructive command blocked by hook"
  }
}

permissionDecision can also be allow, ask or defer. When several hooks answer, the order is deny, then defer, then ask, then allow.

The hooks real plugins ship

Here are the top 10 Claude Code events by the number of repos that use them, from our November 14, 2026 scan. We left out event names from other agents' formats, such as lowercase postToolUse and BeforeTool. We also left out modules (55 repos). It is not an event: in a plugin's hooks/hooks.json it points to a mod's code.

Event Repos Typical use
SessionStart 304 Load context or set up the environment
PreToolUse 226 Guard Bash commands before they run
PostToolUse 224 Format and lint files after edits
Stop 175 Run checks or save state when Claude finishes
UserPromptSubmit 154 Add context to each prompt
SessionEnd 93 Save memory or clean up (MemPalace, oh-my-openagent)
PreCompact 72 Save state before compaction (MemPalace)
SubagentStop 59 Hooked by claude-mem and mem0
PostToolUseFailure 50 Hooked by mem0 and everything-claude-code
SubagentStart 45 Hooked by mem0 and ruflo

Some real examples behind those rows:

  • Superpowers has one hook. On startup|clear|compact, its session-start script reads the using-superpowers SKILL.md and puts it in Claude's context.
  • React runs cat .claude/instructions.md on SessionStart outside the compiler folder. Supabase installs packages at session start and runs a format and lint script after edits.
  • Bun has a pre-bash guard that returns the same JSON deny shape shown above, plus a post-edit formatter.
  • tldraw blocks yarn in Bash and tells Claude the repo uses pnpm. Its Stop hook runs Prettier on changed files.
  • claude-mem hooks Setup, SessionStart, UserPromptSubmit, PostToolUse, Stop and more to record and reload memory. mem0 hooks nine Claude Code events for the same kind of job, and MemPalace runs scripts on Stop, SessionEnd and PreCompact.
  • Pake uses PostToolUse to tell Claude, through additionalContext, that dist/cli.js must be rebuilt when files in bin/ change.
  • claude-code-best-practice wires 30 events to a Python hooks script, which makes it a handy reference for event names.

How to add a hook

All three configs below are examples. Each goes in .claude/settings.json in your project root. If you use more than one, merge them under a single "hooks" object.

Format files after every edit

This is the formatter from the hooks guide. It runs Prettier on each file Claude writes or edits (it needs jq and Node).

  1. In .claude/settings.json, add:

    {
      "hooks": {
        "PostToolUse": [
          {
            "matcher": "Edit|Write",
            "hooks": [
              {
                "type": "command",
                "command": "f=$(jq -r '.tool_input.file_path') && npx prettier --write --ignore-unknown \"$f\""
              }
            ]
          }
        ]
      }
    }
    

Block a dangerous Bash command

  1. Save this script as .claude/hooks/block-rm-rf.sh:

    #!/bin/bash
    COMMAND=$(jq -r '.tool_input.command // empty')
    
    if echo "$COMMAND" | grep -q 'rm -rf'; then
      jq -n '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: "Destructive command blocked by hook"}}'
    fi
    
    exit 0
    
  2. In your terminal, at the project root, make it executable:

    chmod +x .claude/hooks/block-rm-rf.sh
    
  3. In .claude/settings.json, register it:

    {
      "hooks": {
        "PreToolUse": [
          {
            "matcher": "Bash",
            "hooks": [
              {
                "type": "command",
                "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh"
              }
            ]
          }
        ]
      }
    }
    

The simpler way also works: print a reason to stderr and exit 2. Claude sees the reason and can try another approach. A string match like this one is easy to get around, so treat it as a guard against mistakes and keep your permission deny rules in place.

Load context at session start

Plain text from a SessionStart hook goes into Claude's context. This one shows the last five commits on every start, resume, clear and compaction.

  1. In .claude/settings.json, add:

    {
      "hooks": {
        "SessionStart": [
          {
            "matcher": "startup|resume|clear|compact",
            "hooks": [
              {
                "type": "command",
                "command": "git log --oneline -5 2>/dev/null || true"
              }
            ]
          }
        ]
      }
    }
    

Check that the hooks loaded

  1. Inside Claude Code, type:

    /hooks
    

Install ready-made git guards

The git-guardrails-claude-code skill sets up hooks that block git push, reset --hard, clean, branch -D and similar commands. It has about 549,000 skills.sh installs (as of November 13, 2026).

  1. In your terminal, install it:

    npx -y skills add mattpocock/skills --skill git-guardrails-claude-code --agent claude-code
    

Read hooks before you install a plugin

The hooks reference warns: "Command hooks execute shell commands with your full user permissions." When you install a plugin that ships a hooks/hooks.json, its commands run on your machine on every matching event. Open that file and the scripts it calls before you install. Ten hook commands in our scan run curl or wget directly, and scripts can reach the network too.

In an interactive session, Claude Code holds back settings-file hooks until you accept the workspace trust dialog for the folder. In -p or SDK sessions there is no dialog, so a repo's committed .claude/settings.json hooks run. Before you script claude -p over a repo you did not write, the docs suggest reviewing its .claude/ files or turning hooks off for that run.

  1. In your terminal, run Claude Code with hooks off (replace your prompt with your task):

    claude -p --settings '{"disableAllHooks": true}' "your prompt"
    

Our Claude Code security post covers more of this.

Where to start

Start with the PostToolUse formatter. It runs after the edit, so it cannot block anything, and you can check it on the next file Claude edits. Add a PreToolUse Bash guard next, since 143 repos in our scan already do. For panes or rules that rewrite tool calls, Claude Code mods register function hooks instead. To see how others combine hooks with skills, browse skills on CCM and open a repo's .claude/settings.json.

More from the blog