Claude Code Hooks Cheat Sheet

This page condenses Claude Code hooks into one page: events, matchers, exit codes, and Node scripts that block commands, format files, and notify you. You can also download the information as a printable cheat sheet:

Free Bonus: Claude Code Hooks Cheat Sheet

Get the Claude Code Hooks Cheat Sheet (PDF), a printable reference to hook anatomy, lifecycle events, reading the event payload, exit codes, auto-formatting, notifications, and JSON replies:

Claude Code Hooks Cheat Sheet

Want a guided path instead of a reference? Start learning at realjs.com.

Hook Anatomy

  • A hook is a command that runs on an event, filtered by a matcher
  • It fires every time, no matter what the model decides
  • Omit matcher or use "*" to match every tool
  • "Bash" matches one tool, "Write|Edit" several, "^Notebook" is a regex
  • Hooks merge across files; commit .claude/settings.json to share them
Settings file Scope
~/.claude/settings.json All your projects
.claude/settings.json This project, shareable
.claude/settings.local.json This project, gitignored
Language: JSON Filename: Register a Hook
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "node hooks/guard.mjs"
          }
        ]
      }
    ]
  }
}

Lifecycle Events

  • Exit 2 can block PreToolUse, UserPromptSubmit, Stop, and PreCompact
  • PostToolUse can’t block; the tool already ran
  • Tool hooks also fire inside subagents (agent_id is set)
Event Fires
SessionStart A session begins or resumes
UserPromptSubmit You submit a prompt
PreToolUse Before a tool call runs
PostToolUse After a tool call succeeds
Notification Claude Code needs your attention
Stop Claude finishes a response
PreCompact Before context compaction
SessionEnd The session terminates

Read the Event

  • The event arrives as JSON on standard input
  • Always present: hook_event_name, session_id, cwd
  • Tool events add tool_name and tool_input
Language: JavaScript Filename: Parse the Payload
import { readFileSync } from "node:fs";

const raw = readFileSync(0, "utf8");
const event = JSON.parse(raw);
event.hook_event_name // 'PreToolUse'
event.tool_name       // 'Bash'
event.tool_input // { command: 'yarn add' }

Exit-Code Contract

  • exit 1 does not block. Only 2 does
  • Steer, don’t just deny: tell Claude what to run instead
  • Stderr on exit 0 goes to the debug log only; Claude never sees it
  • A hook that hits its timeout (600 s default) is discarded; the action proceeds
Exit code Effect
0 Proceed; stdout may carry JSON
2 Block; stderr goes to Claude as feedback
Other Non-blocking error; the action proceeds
Language: JavaScript Filename: Block yarn, Suggest npm
import { readFileSync } from "node:fs";

const raw = readFileSync(0, "utf8");
const event = JSON.parse(raw);
const cmd = event.tool_input?.command ?? "";
if (/^yarn( |$)/.test(cmd)) {
  console.error("Use npm instead of yarn.");
  process.exit(2); // Blocked
}
process.exit(0); // Allowed
Language: Shell Filename: Test the Guard Without Claude
$ echo '{"tool_input":{"command":"yarn"}}' \
  | node hooks/guard.mjs; echo "exit $?"
Use npm instead of yarn.
exit 2

Auto-Format Every Edit

  • Match "Write|Edit" on PostToolUse
  • Hooks inherit your shell PATH, not node_modules/.bin, so call tools with npx
  • Use spawnSync, which returns errors instead of throwing
  • PostToolUse also sees tool_response, the tool’s result
  • Files rewritten by a Bash command don’t trigger Write|Edit hooks
Language: Shell Filename: Install the Tools Locally
$ npm install --save-dev prettier
Language: JavaScript Filename: Format the Changed File
import { existsSync, readFileSync }
  from "node:fs";
import cp from "node:child_process";

const raw = readFileSync(0, "utf8");
const event = JSON.parse(raw);
const path = event.tool_input?.file_path;
if (path?.endsWith(".js")
    && existsSync(path)) {
  cp.spawnSync("npx", [
    "prettier", "--write", path,
  ]);
}

Notify When Claude Stops

  • Register under "Stop", which fires after every response, not just the last
Language: JavaScript Filename: Desktop Notification
import { readFileSync } from "node:fs";
import cp from "node:child_process";
import { platform } from "node:os";

readFileSync(0); // Consume the event
const msg = "Claude finished responding";
const note = `display notification "${msg}"`;
const [bin, ...args] =
  platform() === "darwin"
    ? ["osascript", "-e", note]
    : ["notify-send", "Claude Code", msg];
if (cp.spawnSync(bin, args).error) {
  console.log(msg); // No notifier
}

Structured JSON Replies

  • Exit 0 and print one JSON object for finer control
  • Stop and PostToolUse: {"decision": "block", "reason": "..."}
Language: JavaScript Filename: Allow, Deny, or Ask on PreToolUse
const reply = {
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "ask", // allow, deny
    permissionDecisionReason: "Edits prod",
  },
};
console.log(JSON.stringify(reply));

Troubleshooting

  • Not firing? Check the event name, the matcher, and the JSON commas
  • /hooks lists what actually loaded and from which file
Language: Shell Filename: Trace a Hook in the Debug Log
$ claude --debug-file /tmp/hooks-debug.txt
$ grep -A2 "Hook" /tmp/hooks-debug.txt
[DEBUG] Hook PreToolUse:Bash error:
Use npm instead of yarn.
[DEBUG] Hook denied tool use for Bash

You can download this information as a printable cheat sheet:

Free Bonus: Claude Code Hooks Cheat Sheet

Get the Claude Code Hooks Cheat Sheet (PDF), a printable reference to hook anatomy, lifecycle events, reading the event payload, exit codes, auto-formatting, notifications, and JSON replies:

Claude Code Hooks Cheat Sheet