"I want auto-formatting after every edit." "Never let anything rewrite .env or package-lock.json." "Stop dangerous rm -rf commands." The way to make these "this must always happen" rules real—without depending on the AI's whims—is Claude Code hooks. A hook is a shell command that runs automatically at specific points in Claude Code's lifecycle, and hooks are also a building block of plugins.

This article lays out, based on the official documentation, what hooks are, the event list, how to configure them, the input/output contract, use cases, and security. Three points up front. ① A hook is "deterministic logic the harness (Claude Code itself) runs"—it fires for sure, without waiting for the model to "decide" to do it. ② You write hooks in settings.json as event name → matcher → command. ③ Events like PreToolUse can "block" via exit code 2 or JSON—stopping edits to protected files or dangerous commands.

CLAUDE CODE · HOOKS

Fires "for sure" at key lifecycle points

— the harness runs it deterministically, not the model's judgment

SessionStart Session begins. Output is injected as context
UserPromptSubmit When a prompt is submitted [can block]
PreToolUse Just before a tool runs = the gatekeeper [can block]
PostToolUse After a tool succeeds = auto-format / audit
Stop When a response ends = keep going until tests pass, etc. [can block]

At each point your shell command runs. The classics: PreToolUse to turn dangerous actions away at the door, PostToolUse to auto-format.

1. What are Claude Code hooks?

The official definition reads: "Hooks are user-defined shell commands that execute at specific points in Claude Code's lifecycle. They provide deterministic control over Claude Code's behavior, ensuring certain actions always happen rather than relying on the LLM to choose to run them." You use them to enforce project rules, automate repetitive tasks, and integrate Claude Code with your existing tools.

The core is determinism. Ask Claude to "run the tests" and whether it does depends on its "judgment." But with a hook, the harness always runs it—no whim enters. Beyond commands (command), there are now also handler types like HTTP, MCP tools, LLM prompts, and subagent verification—but to start, it's enough to think of hooks as "a mechanism to always run a shell command at key points."

2. The hook events

An event represents "where in the lifecycle it fires." Start with the nine classics ("can block" = you can stop that action via exit code 2 or JSON).

EventFires whenBlock
SessionStartA session begins or resumes (stdout is injected as context)No
UserPromptSubmitYou submit a prompt, before Claude processes itYes
PreToolUseJust before a tool call (the primary gatekeeper)Yes
PostToolUseAfter a tool succeeds (the action itself can't be undone)Yes
NotificationOn a notification (awaiting input/permission, etc.)No
StopWhen Claude finishes responding (block = keep working)Yes
SubagentStopWhen a subagent finishesYes
SessionEndWhen a session terminatesNo
PreCompactBefore context compactionYes

The 2026 docs add many more events—PermissionRequest, PostToolUseFailure, ConfigChange, FileChanged, WorktreeCreate, and others. But event names can be added or changed between versions, so this article anchors on the stable nine classics plus the contract in the next section (check the official docs for the full current list).

3. How to configure hooks

You write hooks under the "hooks" key in settings.json. The location sets the scope: user (~/.claude/settings.json) / project (.claude/settings.json, git-shareable) / local (.claude/settings.local.json) / managed policy (organization) / a plugin's hooks/hooks.json. The JSON shape looks like this.

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

The structure is "hooks → event name → an array of { matcher, hooks:[...] } → each hook has type + command." The matcher specifies the target tool name: "*" (or omitted) matches all, "Edit|Write" is a |-separated list, and other characters are regex (e.g. mcp__memory__.*). It is case-sensitive. The /hooks command lists configured hooks but is read-only—to add or change, edit settings.json directly. Disable all with "disableAllHooks": true.

4. The input/output contract

A hook receives JSON on standard input (stdin) and returns its result via the exit code or JSON on standard output.

Input (stdin): common fields include session_id, transcript_path, cwd, and hook_event_name. Tool events add tool_name and tool_input (e.g. {"command":"rm -rf /tmp/build"}); UserPromptSubmit adds prompt. Exit codes: 0 = success (for some events, stdout is added to context—though exit 0 on PreToolUse is not "approval"; the normal permission flow still applies), 2 = block (stderr is passed to Claude as material to adjust, and JSON output is ignored).

# Example: "deny" from PreToolUse via structured JSON output (printed to stdout)
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Database writes are not allowed"
  }
}
# Common fields: continue (false = stop entirely) / decision:"block"+reason /
#                additionalContext (appended for Claude) / updatedInput (rewrite args)

The most important principle: "hooks can tighten restrictions but not loosen them." Returning allow only skips the prompt; deny rules in any scope (including managed settings) always take precedence. A PreToolUse deny blocks even under bypassPermissions / --dangerously-skip-permissions. For the broader permission model, see permission modes and security.

5. Example use cases

The official classics, paired with their configuration.

USE CASES

The classic automations

Auto-format after edits
PostToolUse + Edit|Write runs prettier / a linter automatically.
Protect critical files
PreToolUse blocks (exit 2) edits to .env, .git/, etc.
Stop dangerous commands
PreToolUse detects rm -rf etc. and returns deny.
Re-inject context
SessionStart stdout re-loads conventions and notes every time (even after compaction).
Notifications & audit log
Notification for desktop alerts; PostToolUse to log command history.
Test before stopping
Stop won't let Claude stop until tests pass (keep working until green).

Note: Claude can also change files via Bash. To catch every change, add a Stop hook that scans the working tree once per turn.

6. Security

Hooks are powerful, but they run arbitrary shell commands automatically with your privileges—do not take this lightly.

⚠️ The official warning

"Hooks execute arbitrary shell commands automatically. You are solely responsible for the security of hooks you configure. USE AT YOUR OWN RISK." Hooks can read files, modify your codebase, exfiltrate data, or run any command—only configure ones you trust completely.

Startup snapshot (a key safety feature): hook configuration is captured at session startup, so mid-session config changes don't take effect. This prevents a malicious prompt or tool output from rewriting your hook config during a session. Apply changes in a new session.

In practice: always validate and quote inputs (extract with jq; unquoted variables can become extra args or shell syntax), use absolute paths and ${CLAUDE_PROJECT_DIR}, don't touch sensitive files like .env or .ssh, and never eval tool output. Hooks run without a controlling terminal (no /dev/tty). In organizations, admins can restrict hooks with allowManagedHooksOnly.

Summary

Claude Code hooks are user-defined shell commands that run automatically at key lifecycle points, making "this must always happen" real and deterministic without depending on the model's judgment. The classic events are the nine: SessionStart / UserPromptSubmit / PreToolUse / PostToolUse / Notification / Stop / SubagentStop / SessionEnd / PreCompact (PreToolUse and others can block—stopping protected-file edits or dangerous commands). You configure them in settings.json under "hooks" as event → matcher → type + command.

The I/O is JSON on stdin, exit code 0 (success) / 2 (block), or structured JSON on stdout. The principle: "you can tighten but not loosen restrictions" (deny always wins). The classic use cases: auto-format, protect files, stop dangerous commands, re-inject context, audit, test before stopping. But because they run arbitrary code, be strict about only trusting safe hooks and validating/quoting inputs, and understand that config is captured at startup. Related: plugins, MCP, Claude Code errors.

FAQ

Q. What are hooks for?
A. They deterministically automate "this must always happen." Things like "format after edits," "never let protected files be rewritten," "stop dangerous rm -rf," and "pass tests before stopping" are run for sure by the harness, without relying on the AI's judgment. A hook is a shell command that runs at key lifecycle points, configured in settings.json.

Q. What events (timings) are there?
A. The classics are nine: SessionStart (begin), UserPromptSubmit (on submit), PreToolUse (just before a tool), PostToolUse (after a tool), Notification, Stop (response ends), SubagentStop, SessionEnd (terminate), PreCompact (before compaction). Of these, PreToolUse, UserPromptSubmit, Stop, PreCompact, and others can block and stop the action. The 2026 release adds many more events, but names can change between versions, so check the official docs for the latest.

Q. Can I stop edits to protected files or dangerous commands?
A. Yes—with a PreToolUse hook. A script inspects the file path or command in tool_input, and if it matches .env, rm -rf, etc., returns exit code 2 (or JSON with permissionDecision:"deny") to block it. Crucially, deny always takes precedence and stops even under bypassPermissions. Hooks only work in the "tighten restrictions" direction.

Q. I changed my config but it isn't taking effect.
A. That's by design (a safety feature). Hook config is captured as a snapshot at session startup, so mid-session changes don't apply. This prevents a malicious prompt or tool output from silently rewriting your hook config. Start a new session to apply it. Note that /hooks is a read-only list; add or change hooks by editing settings.json directly.

Q. Are hooks safe?
A. Not unconditionally. Hooks run arbitrary shell commands with your privileges, which is why the docs say "use at your own risk." Only configure hooks you trust, validate and quote inputs with jq, use absolute paths and don't touch .env / .ssh, and never eval tool output. In organizations, admins can restrict hooks with allowManagedHooksOnly.