> learn / claude-code-hooks

Claude Code Hooks, Explained

Hooks are the extension point that lets Claude Code talk to the rest of your system. This article covers every hook type, the settings.json format, a working example, and the security questions you should ask before running anyone's hook config.

Published: April 15, 2026·12 min read·← All articles

A Claude Code hook is a shell command that Anthropic's coding agent executes automatically at a defined point in its lifecycle — for example, right after it finishes editing a file, right before it runs a shell command, or at the end of an assistant turn. Hooks are declared in a JSON settings file and run with the permissions of the user who launched Claude Code, which makes them powerful and security-sensitive in equal measure.

The seven hook events

As of the April 2026 release, Claude Code exposes seven hook events. They are declared as top-level keys under a hooks object in settings.json.

  • PreToolUse — fires before Claude Code invokes a tool (Edit, Write, Bash, WebFetch, etc.). A hook here can inspect the pending tool input and, by exiting non-zero with a message on stderr, abort the tool call. Common use cases: block commits to main, forbid writes to sensitive paths, enforce a repo-specific linter.
  • PostToolUse — fires after a tool completes. Sees both the tool input and the tool result. Common use cases: log activity (VibeMon lives here), run a formatter, notify a dashboard.
  • UserPromptSubmit — fires when the user presses enter on a prompt, before the model receives it. Useful for redacting secrets from the prompt or injecting project-specific context.
  • Stop — fires when the assistant finishes a turn. A hook here is the canonical place to mark the end of a unit of work.
  • SubagentStop — fires when a subagent (launched via the Agent tool) finishes.
  • SessionStart — fires when a new session begins. Ideal for injecting warm-up context or writing a session marker.
  • Notification — fires on user-visible notifications (permission prompts, idle warnings). Use this to route Claude Code alerts to your OS notification center or a Slack channel.

Where hook config lives

Claude Code merges hooks from three files, in order of specificity:

  1. ~/.claude/settings.json — user-global. Applies everywhere.
  2. .claude/settings.json (in the project root) — project-wide. Committed to the repo so teammates get the same behavior.
  3. .claude/settings.local.json — per-developer overrides. Typically added to .gitignore.

When multiple layers define the same hook event, the entries are concatenated. This means you can ship a team-level logging hook in .claude/settings.json and add a personal formatter hook in .claude/settings.local.json without either stepping on the other.

The JSON structure

Every hook event takes an array of matchers. Each matcher has a regex string (matcher) that filters which tool invocations the hook cares about, and a list of commands (hooks) to run when a match occurs. A missing or empty matcher matches everything.

~/.claude/settings.json — excerpt
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "curl -s -X POST https://vibemon.dev/hook -H 'Authorization: Bearer $VIBEMON_TOKEN' -d '{\"event\":\"post_tool_use\"}'"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "curl -s -X POST https://vibemon.dev/hook -d '{\"event\":\"stop\"}'" }
        ]
      }
    ]
  }
}

The command string is executed by the user's default shell. Claude Code sets several environment variables before invoking it, including CLAUDE_TOOL_NAME, CLAUDE_TOOL_INPUT, and CLAUDE_TOOL_RESULT, and streams the tool's raw JSON to the command's stdin. A hook that wants structured access reads stdin rather than parsing environment variables.

Exit codes and feedback

Hooks communicate back to Claude Code through exit codes and stderr:

  • Exit 0 — success. The tool call proceeds (for PreToolUse) or the turn continues.
  • Exit 2 — blocking failure. For PreToolUse, Claude Code cancels the tool call and feeds the hook's stderr back to the model as an error signal. This is how you enforce a policy without Claude needing to know the policy.
  • Any other non-zero exit — logged as a warning but not blocking.

A working VibeMon example

VibeMon uses PostToolUse to record an event every time Claude Code writes a file, plus Stop to mark the end of a turn. The install script adds a single hook block to ~/.claude/settings.json and pairs the machine with your VibeMon account via a one-time token. From the moment the config lands, every subsequent edit feeds a drop to your slime — no further action required.

The same script detects Cursor, Gemini CLI, and Codex installations and writes the equivalent hook block for each (Cursor uses a .cursor/rules or .cursor/hooks convention; Gemini CLI uses ~/.gemini/settings.json; Codex CLI uses ~/.codex/settings.json). From VibeMon's perspective the four agents are interchangeable — the hook endpoint normalizes them all into a single hook_event row.

Security considerations

Because hooks execute shell commands as your user, a malicious hook config is equivalent to a malicious shell script: it can read your SSH keys, exfiltrate environment variables, delete files, or install backdoors. Before installing any hook config from the internet — VibeMon's included — apply at minimum these checks:

  • Read the command. If it ends in | bash or | sh, read the fetched content first.
  • Scope the hook. Prefer .claude/settings.local.json over user-global when testing third-party hooks, so the blast radius is a single project.
  • Inspect network destinations. Any hook that POSTs to a domain other than the one you expected should be considered hostile.
  • Assume hook data is sensitive. Even if a hook legitimately logs events, that log may contain file paths, repo names, and branch names that identify proprietary projects. VibeMon addresses this by hashing project identifiers and never transmitting file contents.

Related work in other agents

The hook concept is not unique to Claude Code. Cursor exposes hooks through .cursor/hooks files, Gemini CLI reads ~/.gemini/settings.json, and Codex CLI follows a similar convention at ~/.codex/settings.json. Aider and Cline take different approaches (middleware and scripts rather than JSON config) but expose comparable extension points. An agent-agnostic hook spec is a long-standing community request and has not yet converged.

Further reading

  • What is vibe coding? — the broader workflow that hooks enable.
  • Custom Slime API reference — the endpoint VibeMon's hook talks to.
  • Setup guide — a three-step walkthrough that installs the hook config for every supported agent.
  • FAQ — privacy, data scope, and what the install script actually writes.

Anthropic occasionally reorganizes hook event names across minor Claude Code releases. This article tracks the April 2026 API. We log breaking changes in the changelog.