Engineering Guide

Claude Code Hooks: Deterministic Control (Not Just Shell Scripts)

Hooks are the part of Claude Code that does not depend on the model remembering anything. They are configured handlers — shell commands, HTTP calls, MCP tool calls, and model-judgment prompts — that the harness runs at named points in a session, whether or not the assistant decides to. This guide covers the events, the five handler types, hooks versus skills, and, as of 2026-10-03, the new kind of hook Anthropic shipped in Claude Code 2.1.287: mods, built from JavaScript function hooks that run inside Claude Code itself.

Correction, October 3, 2026: single-model benchmark latency is a mean, not a median. Labels were corrected; measured values and the original run dates are unchanged. Measurement details.

Diagram of Claude Code hooks firing at lifecycle events

What hooks actually are

The official definition: hooks are user-defined handlers that execute at specific points in Claude Code's lifecycle, providing deterministic control so certain actions always happen rather than relying on the LLM to choose to run them.

Deterministic is the key word. If you put "always run prettier after editing a file" in your project instructions or a CLAUDE.md, the model may or may not do it, depending on how full the window is. In a PostToolUse hook, the harness runs it after every matching edit; the model cannot forget what it never had to remember.

Configuration lives in a settings JSON file under a single top-level hooks object. Each key is an event name that maps to an array of entries shaped like { matcher, hooks: [{ type, command }] }. Scopes are ~/.claude/settings.json (every project), .claude/settings.json (shared with the team) and .claude/settings.local.json (private); managed policy, plugins and skill frontmatter can add more. Since 2.1.287, Anthropic's mods pages call this kind a settings hook, to tell it apart from a mod's function hooks — more on that below. Our Claude Code walkthrough shows where these files live.

The lifecycle events

Older posts list nine "classic" events. When we read the official Hooks reference on 2026-10-03 we counted 33 events, including newer ones such as Setup, UserPromptExpansion, PermissionRequest, PermissionDenied, PostToolBatch, MessageDisplay, TaskCompleted, FileChanged, PreModelSwitch and PostModelSwitch. Some are version-gated; the count is a dated figure.

For everyday work, anchor on a small, stable core:

Core lifecycle events (can it block?) SessionStart UserPromptSubmit PreToolUse PostToolUse Notification Stop Fires as a session opens. stdout is injected as context. On prompt submit, before Claude reads it. Exit 2 erases the prompt. Before a tool runs. Exit 2 or a deny decision blocks the call. After a tool succeeds. Cannot block: the tool already ran. When Claude needs input or permission. No blocking. When Claude finishes. Exit 2 forces it to keep going. blue = can block / gate
Blocking behavior is per-event, not global. Chart: DataLLM Lab

Exit code 2 blocks a PreToolUse call, erases a UserPromptSubmit prompt, and prevents Stop from ending the turn, but on PostToolUse it only shows stderr to Claude, because the tool already ran. There is no blanket "exit 2 blocks everything" rule.

Beyond shell scripts

"Hooks are just shell commands" is the most outdated claim in circulating explainers. The reference lists five handler type values:

Per the Hooks reference, read 2026-10-03, default timeouts differ: 600 seconds for command, http and mcp_tool, 30 for prompt, 60 for agent. A prompt hook takes an optional model field that defaults to the model Claude Code uses for background work. The docs do not say it accepts arbitrary non-Anthropic models, so an earlier version of this page that suggested pointing it at DeepSeek V4 Flash overstated things. If you want a non-Claude judge, the documented route is an http handler aimed at an endpoint you control, which can call whatever model you like.

Mods and function hooks (added 2026-10-03)

Claude Code 2.1.287 added a second kind of hook. The changelog line reads "Added Claude Mods: plugins may now modify deeper behavior." Everything below was read on Anthropic's own changelog and Claude Code docs on 2026-10-03, unless the tier says otherwise.

ClaimTierSource
Mods shipped in Claude Code 2.1.287ConfirmedAnthropic changelog; the GitHub release for v2.1.287 is timestamped 2026-10-01
A mod is a plugin whose JavaScript or TypeScript event handlers, called hooks, run inside Claude Code and can observe, rewrite or answer an eventConfirmedMods overview, Claude Code docs
Mods can change prompts, tool calls, turns and what the interface drawsConfirmedMods overview and reference
On by default from 2.1.287; the early-access CLAUDE_CODE_ENABLE_FUNCTION_HOOKS variable is now ignoredConfirmedMods overview
You should know: a built-in mod where a side agent flags things you or Claude might miss; off by defaultConfirmedChangelog 2.1.287 and the built-in mods table
2.1.288 added $.ui.selection() for modsConfirmedChangelog; release timestamped 2026-10-02
Release date of October 1Attributedccleaks.com, read 2026-10-03; matches the GitHub timestamp

Nothing here rests on unsourced reporting.

What changes. A settings hook runs outside Claude Code; a function hook is a JavaScript function called in Claude Code's own process, middleware-style: it receives the event and a next function, and can pass the event on, pass on a rewritten copy, or answer it itself. The documented events go well past the settings list: tool.call and tool.check around every tool call, prompt.submit and prompt.section for the prompt and individual system-prompt sections, turn.step before each model request (it can swap the model or effort), and ui.render to redraw parts of the interface such as the spinner or a tool call's row. Mods can also draw panes, a band above the prompt and toasts, and see every settings hook event as classic.<Event>.

What does not change. Anthropic's admin page says settings hooks keep working alongside mods and nothing about them is deprecated.

The catch for anyone relying on hooks as guardrails. Mods are not sandboxed and run with your permissions. The docs say a mod can approve a tool call that an ask rule would prompt for, or that one of your own PreToolUse hooks blocked. Order matters too: a PreToolUse hook in managed settings runs before any mod and its block is final, but PreToolUse hooks from other settings files and plugins run after the last mod, so a mod that answers a tool call itself keeps them from running. So the guarantee in the first section is now absolute only for managed hooks; a project hook is deterministic relative to the mods you installed. Where Claude Code's built-in guard loads (machines with managed settings, or a Team or Enterprise sign-in), deny rules still beat a user's mod. Before installing one, run claude plugin validate on it to list the events it handles and the APIs it calls. To switch installed mods off, disable the plugin, start with --safe-mode, or set disableAllHooks, which also stops your settings hooks; none of these stops the mods built into Claude Code.

You should know is the built-in mod the 2.1.287 changelog names. Per the docs, a side agent watches longer tasks and posts a note above the prompt; it is off by default, and the changelog gives /plugin enable cc-plugin-you-should-know@builtin as the switch, and the docs say it appears in /plugin only if it is available for your organization.

Hooks vs skills vs slash

Anthropic merged custom slash commands into skills: .claude/commands/deploy.md and .claude/skills/deploy/SKILL.md both create /deploy. Slash is now one way to invoke a skill, not a separate primitive.

A skill is a SKILL.md file: frontmatter with a description that tells Claude when to use it, plus markdown instructions. Claude loads it when relevant, or you run it by name; disable-model-invocation: true makes it manual-only. Our skills deep-dive covers authoring.

DimensionSettings hookMod (function hook)Skill / slash
Who initiatesThe harness, at an eventThe harness, at an eventClaude when relevant, or you by name
What you writeA script plus a settings.json entryJavaScript or TypeScript in a pluginMarkdown
Model token cost to stay activeZero for command and httpZero unless it calls a model~0 until fired
Can blockYes; final only in managed settingsYes, and can approve tooNo
Can draw in the interfaceNoYesNo
Best forGuardrails, formatting, loggingPanes, custom commands, rewriting eventsReusable methods and checklists

Skills are model- or human-initiated. Hooks of both kinds are harness-initiated: the settings hook is the boring, auditable one; the mod is the powerful one you review before installing.

Config recipes

Three examples from the getting-started guide. First, a desktop notification when Claude needs you:

{
  "hooks": {
    "Notification": [
      { "matcher": "", "hooks": [
        { "type": "command",
          "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'" }
      ] }
    ]
  }
}

Second, format-on-edit. Matchers are tool names and can be piped, so Edit|Write catches both. An empty matcher or * matches all; a matcher with any other special character is read as an unanchored JavaScript regular expression, and MCP tools match as mcp__<server>__<tool>.

{
  "hooks": {
    "PostToolUse": [
      { "matcher": "Edit|Write", "hooks": [
        { "type": "command",
          "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
      ] }
    ]
  }
}

Third, blocking a dangerous command. A PreToolUse hook matched on Bash inspects .tool_input.command with jq and, when it sees something destructive, exits 0 and prints JSON with hookSpecificOutput.permissionDecision set to deny plus a permissionDecisionReason. Instead of denying, a PreToolUse hook can return updatedInput under hookSpecificOutput to rewrite the tool's arguments before it runs. The if field (permission-rule syntax such as Bash(rm *)) narrows the match without a script. If this guard must survive mods, ship it in managed settings. To see what is configured, /hooks opens a read-only browser that labels where each hook comes from; to change anything, edit the JSON. For routing between models, see our Claude Code router and sequential-thinking guides.

Build the hook-driven gate, keep the bill flat

An http hook can call any model behind an OpenAI-compatible endpoint. DataLLM Lab gives you 300+ models behind one key at datallmlab.com/v1, so a gate can use a small fast model while your session uses Opus.

A cheap judge, measured

A gate that calls a model on every tool use multiplies its price and latency by your tool-call count. Four models on our nine-task benchmark, all 9 out of 9:

Derived cost per 1,000 tasks, all 9/9 (priced 2026-10-02) Solar Mini 4 $0.03 · 2s mean GPT-6 Luna $0.16 · 4.9s Claude Sonnet 5.5 $1.95 · 3.4s Claude Opus 5.5 $4.03 · 4.8s Scale: 130 px per $1 per 1,000 tasks; every width = value × 130. Cost derived from measured tokens × list price on 2026-10-02.
Solar Mini 4 was the cheapest and fastest of the 75 models that scored 9/9 on our board as of 2026-10-03. Chart: DataLLM Lab

At the 2026-10-02 list prices, Opus 5.5 cost $4.03 / $0.03, or about 134 times, what Solar Mini 4 did for the same nine passes. Solar Mini 4 is priced at $0.05 in and $0.2 out per million tokens (2026-10-02).

What this does not tell you. Our tasks ask a model to write small, self-contained Python functions. A gate asks it to classify a shell command. Nine self-contained functions cannot separate a frontier model from a competent small one — all four of these passed everything. The chart gives you a price and latency floor, not proof of judgment. Test a cheap judge on your own blocked-command log first; note a mod hook gets 10 seconds of its own execution time per event, per the mods reference read 2026-10-03.

Method. Nine Python tasks; generated code runs against hidden asserts in an isolated python3 -I subprocess with a 12-second timeout. Temperature 0, max_tokens 4000, one scored attempt per task; an API-layer failure is recorded separately from a wrong answer. Cost is derived from measured token counts times list price, not a billing statement. Runs go through OpenRouter. Full detail on the methodology page; for API-reported router costs on the same tasks, see our Jev Router test.

Which one to reach for

If a behavior must be guaranteed and can be written as a rule, use a settings hook — free, deterministic, and auditable in one JSON file; put it in managed settings if it must outrank mods. If you need to see or reshape what Claude Code does — a pane, a rewritten prompt section — write a mod, and review it like any code running with your permissions. If it is a reusable method that needs the model's judgment about when to apply it, write a skill, and give it a slash name to keep it dormant until you call it. Reserve CLAUDE.md for always-relevant context; you pay for it every turn. On Claude Opus 4.8 or Qwen3 Coder Next, guardrails in hooks keep the window lean. For the security side of letting code approve tool calls, see our agent security piece.

FAQ

What are Claude Code hooks?

User-defined handlers that execute at specific points in Claude Code's lifecycle, giving deterministic control so certain actions always happen instead of relying on the model to choose to run them. They live in a settings JSON file under a single top-level hooks object keyed by event name.

What events can a hook fire on?

The stable core is SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Notification, Stop, SubagentStop, PreCompact and SessionEnd. The official reference now documents around thirty events in total, some version-gated, so treat the full count as current-docs rather than fixed.

Are hooks just shell commands?

No. There are five handler types: command, http, mcp_tool, prompt and agent. The prompt and agent types let a hook make a judgment call with a model, not just match a fixed pattern. The agent type is newer.

How do I block a tool call?

On a PreToolUse hook, exit code 2 blocks the tool and sends stderr back to Claude as the reason. For finer control, exit 0 and print JSON with hookSpecificOutput.permissionDecision set to deny or allow. Exit 2 does not block PostToolUse, since the tool has already run.

Hook or skill — which should I use?

A hook is run by the harness every time at a lifecycle event, costs zero model tokens, and can guarantee or block. A skill is a reusable method Claude loads when relevant, or that you invoke by name. Since slash commands merged into skills, slash invocation is just one way to trigger a skill.

Where do hook configs live?

In settings JSON at three scopes: the user file for all projects, a shareable project file, and a private local project file. Plugins, managed policy, and skill or agent frontmatter can also add hooks. Browse them read-only with /hooks and edit the JSON directly.

What are Claude Code mods, and are they the same as hooks?

Mods shipped in Claude Code 2.1.287. They are built from JavaScript function hooks that run inside Claude Code itself and can change prompts, tool calls, turns and what the interface draws. The hooks this page covers are now called settings hooks, to tell them apart. Mods are not sandboxed and run with your permissions, so a mod can override decisions a settings hook or permission rule would make — see the section above.

Written by

Founder of DataLLM Lab, the unified LLM gateway. Kevin tests models the boring way — same prompts, real costs, unedited outputs — and writes up what the runs actually show. Articles are drafted with AI assistance and published under his name; every first-party number comes from an executed run.

Written by

Founder of DataLLM Lab, the unified LLM gateway. Kevin tests models the boring way — same prompts, real costs, unedited outputs — and writes up what the runs actually show. Articles are drafted with AI assistance and published under his name; every first-party number comes from an executed run.

One API for every model

One API, every model.

Get a single API key for Claude Opus 4.7, GPT-5.4, and 300+ more — with automatic price comparison and routing to the best model for every request.