“The rules are in CLAUDE.md, but Claude still ignores them.” If you run Claude Code with a team, you will hit this wall. CLAUDE.md is a polite request — it must be designed with the expectation that it will occasionally be ignored. Our team rolled out Claude Hooks in January 2026. Before that, we kept burning review cycles on format-only diffs in pull requests and manually stopping Claude from editing .env files. Within days of landing hooks, those back-and-forths almost vanished — review comments dropped by roughly 30% from my gut-feel tracking. This guide walks through the hooks I actually run in production: ready-to-paste settings.json snippets, the stdin/exit-code contract, how to debug when nothing fires, and a phased team rollout playbook. The canonical references are the Hooks reference and the Automate workflows with hooks guide.
Handler type Runtime Typical use Can block?
commandShell script / external binaryValidation, auto-format, loggingYes (exit 2)
httpHTTP POST requestAudit logs, SIEM integrationYes
promptSub-prompt to ClaudeLLM-driven judgment gatesYes
agentSpawned sub-agentMulti-turn review, workflow gatesYes
📑Table of Contents
  1. What Claude Hooks Actually Are — and Why CLAUDE.md Is Not Enough
  2. Hooks vs Skills vs CLAUDE.md vs MCP — When to Use Which
  3. Complete Claude Hooks Event Reference (April 2026)
  4. The Hook I/O Contract: stdin, Exit Codes, and JSON Output
  5. Eight Claude Hooks You Can Copy Today
  6. Team Rollout Playbook: Phase 1 → 3
  7. When Your Hook Doesn’t Fire — Debugging Checklist
  8. Security Checklist You Must Read
  9. Execution Order, Timeouts, and Concurrency
  10. Frequently Asked Questions
  11. Wrapping Up — Start With One Hook

Source: Claude Code Hooks Guide (as of April 2026)


What Claude Hooks Actually Are — and Why CLAUDE.md Is Not Enough

Claude Hooks are event handlers that run at deterministic points in Claude Code’s lifecycle. Where CLAUDE.md is an instruction the model chooses whether to follow, hooks are system-level guardrails that fire whether Claude wants them to or not. When we ran CLAUDE.md alone, the rule “never edit .env” was written plainly at the top of the file. Claude still proposed writing to it a handful of times per week. After I added a hook that rejects writes to .env paths, that incident class dropped to zero.

CLAUDE.md (a request)

Context that Claude reads when it feels like it. Forgotten in long sessions, skipped when the model is confident, inconsistent across team members.

Claude Hooks (a guarantee)

Deterministic handlers triggered by lifecycle events. They fire regardless of what Claude intends to do.

Configuration files and scope

Hook configuration lives in a layered settings.json. From narrowest to broadest:
  • .claude/settings.local.json — personal, typically gitignored
  • .claude/settings.json — project, committed to git for team sharing
  • ~/.claude/settings.json — global, all projects
  • Managed policy settings — org-wide, enforced, cannot be disabled by users
For team work, committing .claude/settings.json is the baseline. Security-critical rules belong in Managed Policy so they cannot be turned off.

⚠️ Hooks are snapshotted at session start

Editing settings.json mid-session has no effect until you restart Claude Code. This is also a security property: a compromised process cannot rewrite its own guardrails without dropping the session.


Hooks vs Skills vs CLAUDE.md vs MCP — When to Use Which

Claude Code has several extension points and the “which one do I reach for?” question comes up constantly. Here is how I split them in practice.

My rule of thumb: safety and quality rules that must not be broken go into hooks. Reusable workflows go into skills. Tone, style, and “it would be nice if you did X” preferences stay in CLAUDE.md. Keeping those three buckets distinct removes most of the ambiguity.

Extension Determinism Typical use case Combines with
Hooks◎ Always runsSecurity enforcement, auto-format, audit logging, destructive-command blocksEverything
CLAUDE.md△ Model discretionTone, style guides, review criteria, project contextHooks (complementary)
Skills△ Model invokesReusable workflows (PR creation, release notes, debugging playbooks)Called from hooks
Slash Commands○ User-triggeredHuman-initiated routinesPair with skills
Subagents△ Model delegatesParallel research, isolated tasksLaunched from agent-type hooks
MCP Servers○ Tool providerExternal DBs, APIs, internal servicesMatcher can target mcp__.*

Source: author experience + Claude Code Hooks Guide (April 2026)

More on skills and plugins is in Best Claude Plugins: 3 Essentials That Transform Your Workflow. Calling a skill from a hook gives you the best of both worlds: enforced execution plus a reusable, versioned body of logic.


Complete Claude Hooks Event Reference (April 2026)

Claude Code exposes over twenty lifecycle events. The important split is between blockable events (you can veto or rewrite) and observational events (fire-and-forget).

🔄 Lifecycle flow

SessionStart → UserPromptSubmit → (loop) → PreToolUse (blockable) → [tool runs] → PostToolUse → … (loop end) → Stop (blockable) → SessionEnd

Event Blockable When it fires / typical use
SessionStartYesNew session. Inject context, run env checks
SessionEndNoSession wrap-up. Save logs, clean up
UserPromptSubmitYesUser message submitted. Prompt gating
PreToolUseYesJust before a tool call. Deny or rewrite input
PostToolUseNoRight after a tool call. Auto-format, audit
NotificationNoPermission prompts / idle waits. Desktop notify
StopYesClaude is about to finish. Gate completion
SubagentStart/StopStop onlySub-agent lifecycle
PreCompactNoBefore context compaction. Preserve state
PermissionRequestYesPermission prompts. Auto-allow / auto-deny

Source: Claude Code Hooks reference (April 2026)

The matcher field is not always a regex. When the string is only alphanumerics, underscores, or | (e.g. Bash, Write|Edit|MultiEdit) it is evaluated as an exact match or |-delimited enum. Only when the matcher contains regex metacharacters (e.g. mcp__memory__.*) is it interpreted as a JavaScript regex. Note that FileChanged treats its watch list as literal filenames — regex is not applied there. The if field is only honored for tool-related events (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied) and lets you filter on argument content beyond the tool name.


The Hook I/O Contract: stdin, Exit Codes, and JSON Output

The thing that trips up most first-time hook authors is the interface between Claude Code and the hook script. Get this wrong and you will watch hooks “fire” without doing anything. Get it right and everything else is mechanical.

The stdin JSON payload

Hook scripts receive a JSON document on standard input. The essentials:
{
  "session_id": "uuid...",
  "cwd": "/path/to/project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/path/to/file.ts",
    "content": "..."
  }
}

In shell scripts, the idiom is to read stdin once and extract fields with jq:

#!/bin/bash
PAYLOAD=$(cat)
FILE=$(echo "$PAYLOAD" | jq -r '.tool_input.file_path // empty')
TOOL=$(echo "$PAYLOAD" | jq -r '.tool_name')

Exit codes and JSON output

A hook communicates back to Claude via its exit code, its stdout JSON, or both.
  • exit 0 — success, continue
  • exit 2 — block. Anything written to stderr is fed back to Claude as feedback, so the next turn sees why it was blocked
  • any other exit code — non-blocking error (treated as a warning)

For structured control, emit JSON on stdout using hookSpecificOutput:

{
  "hookSpecificOutput": {
    "permissionDecision": "deny",
    "permissionDecisionReason": "Writing to .env is blocked by policy"
  }
}

The three values of permissionDecision are "allow", "deny", and "ask". The "ask" variant is useful when you want to auto-allow most cases but surface a permission prompt for the dangerous ones. To rewrite tool arguments without changing Claude’s intent, return an updatedInput field — this is the cleanest way to redact secrets or correct paths.

$CLAUDE_PROJECT_DIR for portable paths

When a hook script lives alongside your project, reference it with $CLAUDE_PROJECT_DIR. This is the one env var you want committed in a shared .claude/settings.json: it makes scripts work on every teammate’s machine regardless of where they cloned the repo.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{
          "type": "command",
          "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/protect-files.sh"
        }]
      }
    ]
  }
}

Eight Claude Hooks You Can Copy Today

Here are eight hooks I either run in production or tested long enough to recommend. Every example is a complete settings.json fragment you can paste as-is.

1. Block destructive shell commands (PreToolUse)

Catches rm -rf /, fork bombs, and git push --force before they run. Because the stderr message is returned to Claude as feedback, the model will typically try a safer approach on the next turn instead of repeating the same mistake.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{
          "type": "command",
          "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-dangerous.sh"
        }]
      }
    ]
  }
}
#!/bin/bash
CMD=$(jq -r '.tool_input.command // empty')
if echo "$CMD" | grep -Eq 'rm -rf /|:\(\)\{|git push.*--force|--no-verify'; then
  echo "Blocked by policy: $CMD" >&2
  exit 2
fi
exit 0

2. Enforce secret-handling rules (PreToolUse)

This is the hook I am most glad I installed. I once had a test API key written into a config file that almost made it into a commit — I caught it during review, but it was too close for comfort. Since then, direct writes to .env, secrets/, and config/*.json are refused outright.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [{
          "type": "command",
          "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/protect-secrets.sh"
        }]
      }
    ]
  }
}
#!/bin/bash
FILE=$(jq -r '.tool_input.file_path // empty')
case "$FILE" in
  *.env|*.env.*|*/secrets/*|*/config/*.json)
    cat <<EOF
{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"$FILE is a secret-bearing file. Use environment variables or a separate config."}}
EOF
    exit 0
    ;;
esac
exit 0

🏆 Author’s note

The moment this hook went live, Claude’s suggestions to hard-code secrets started dying in-flight instead of at review time. For non-engineers using Claude, I pair it with three more guards: no writes to production directories, git push --force blocked, and outbound http hooks disabled. That gives them a box they cannot easily walk out of.

3. Auto-format on save (PostToolUse)

This was the first hook we shipped team-wide. Before it, two teammates ran different prettier configs and every PR carried formatting-only noise that drowned out the real review. Within a week of landing this hook, the “please run the formatter first” back-and-forth evaporated and review comments dropped by roughly 30% from my informal counts.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [{
          "type": "command",
          "command": "jq -r '.tool_input.file_path' | xargs -r npx prettier --write 2>/dev/null || true"
        }]
      }
    ]
  }
}

⚠️ Watch your matcher

I initially set the matcher to a broad Write|Edit and prettier ran against every Markdown edit and config tweak, producing surprise reformats. I debugged it with claude --debug, confirmed which events were firing, then narrowed the hook with an if clause to .ts, .tsx, .js, and .json. Lesson: start narrow, widen only when you are sure.

4. Automatic git checkpoints (PostToolUse)

Commit every file change to a WIP branch so rolling back a Claude experiment is one reset away. I keep the commit message constant so my real history stays clean.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [{
          "type": "command",
          "command": "git add -A && git commit -m 'chore(claude): auto checkpoint' --no-verify -q || true"
        }]
      }
    ]
  }
}

5. Ship audit logs over HTTP (PostToolUse + http)

For SOC2 or internal audit, you want every tool call recorded somewhere your auditors can query. The http handler lets you POST directly without piping through a shell, and async: true keeps it off the critical path.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": ".*",
        "hooks": [{
          "type": "http",
          "url": "https://audit.internal.example.com/claude",
          "method": "POST",
          "async": true,
          "allowedEnvVars": ["AUDIT_TOKEN"],
          "headers": { "Authorization": "Bearer $AUDIT_TOKEN" }
        }]
      }
    ]
  }
}

6. Inject project context at session start (SessionStart)

I load a trimmed version of CONTRIBUTING.md automatically so Claude knows my team’s conventions without me having to remember to paste them.

{
  "hooks": {
    "SessionStart": [{
      "hooks": [{
        "type": "command",
        "command": "echo '{\"additionalContext\":\"'$(cat $CLAUDE_PROJECT_DIR/CONTRIBUTING.md | head -c 2000)'\"}'"
      }]
    }]
  }
}

⚠️ Keep SessionStart light

My first attempt at this hook slurped an entire README and noticeably slowed session startup. Keep SessionStart hooks to cheap context injection and defer any real analysis to later events. Slow SessionStart hooks are the #1 complaint I hear from teams.

7. Desktop notifications when Claude needs you (Notification)

If you multitask the way I do, Claude’s permission prompts disappear under other windows. A notification hook fixes that in one line per OS.

Claude Hooks utilization - permission prompt screen
Claude waiting on a permission prompt. A Notification hook fires osascript so the alert reaches me even when I am in another app.
{
  "hooks": {
    "Notification": [{
      "hooks": [{
        "type": "command",
        "async": true,
        "command": "osascript -e 'display notification \"Claude needs attention\" with title \"Claude Code\" sound name \"Ping\"'"
      }]
    }]
  }
}

If you prefer a native Windows notification, call PowerShell directly from the hook — no WSL required:

{
  "hooks": {
    "Notification": [{
      "hooks": [{
        "type": "command",
        "async": true,
        "command": "powershell -Command \"[System.Windows.Forms.MessageBox]::Show('Claude is waiting', 'Claude Code')\""
      }]
    }]
  }
}

8. Gate completion with a review subagent (Stop + agent)

Attach a subagent to the Stop event so Claude cannot mark work done until a reviewer has looked at the diff. This is the pattern I recommend for teams that want an extra pair of “eyes” on risky refactors.

{
  "hooks": {
    "Stop": [{
      "hooks": [{
        "type": "agent",
        "agent": "diff-reviewer",
        "instructions": "Inspect the latest git diff. If there are bugs or security concerns, return a blocking decision."
      }]
    }]
  }
}

⚠️ Don’t run full test suites on Stop

I tried wiring a full test run into Stop once. Every small edit paid a multi-second tax and the interaction loop felt broken, so I pulled it out. Keep Stop hooks scoped to quick reviews or static analysis, and push heavy test runs to CI where they belong.


Team Rollout Playbook: Phase 1 → 3

Dropping a wall of hooks on a team on day one invites revolt. Here is the ramp my team actually used.

Phase 1 — Try it solo (settings.local.json)

Put PostToolUse format and Notification hooks in your own settings.local.json. Prove the value to yourself. Tell the team it exists but do not impose anything yet.

Phase 2 — Project share (.claude/settings.json)

Commit the auto-format hook, add PreToolUse safety guards, land .env write blocking. The single biggest win in my team was the .env block — it enforced a rule we never had to re-explain.

Phase 3 — Managed Policy (allowManagedHooksOnly)

Lift the security-critical hooks into Managed Policy and turn on allowManagedHooksOnly. Now even an admin user cannot disable them from their local settings. Good fit for SOC2 / regulated orgs. Audit HTTP hooks land here too.


When Your Hook Doesn’t Fire — Debugging Checklist

Hooks have a dozen silent failure modes. Here is the exact order I walk through when a teammate says “my hook isn’t working”:

  1. Open /hooks — the browser is read-only, so you will edit the file directly, but the view confirms what Claude thinks is registered
  2. Launch with claude --debug and watch the event stream. Matching your matcher regex against the actual event name catches half the bugs
  3. Validate settings.json as JSON. A single trailing comma silently disables every hook in the file
  4. Test the regex against the tool names you expect. Broad matchers like Write hit a lot more than you might think
  5. Restart the session. Hooks are snapshotted at session start — editing mid-session does nothing until you restart
  6. Replay the stdin payload locally. I once had a hook that silently returned empty strings because I mistyped a jq key; saving a sample payload to disk and running the script by hand revealed it immediately

Security Checklist You Must Read

Hooks are powerful but they run as unsandboxed shell under your user. Skip these and you expand the attack surface rather than reduce it. More on the broader picture in Claude Code Security Settings Guide.

🔒 Hook security checklist

  • Always quote shell variables ("$FILE") to prevent whitespace and metacharacter injection
  • Use absolute paths or $CLAUDE_PROJECT_DIR. Never rely on relative paths
  • Gate .git/, secrets/, and .env from hook-side access
  • Do not trust third-party hook repos without reading every line
  • Hooks run as your user — no sudo, no root, no privileged ops
  • For http hooks, pin destinations and use allowedEnvVars to scope credentials

Execution Order, Timeouts, and Concurrency

  • Multiple hooks on one event: run in registration order. For PreToolUse, any deny halts the chain
  • Default timeout (varies by handler): command = 600 seconds, prompt = 30 seconds, agent = 60 seconds. Exceeding the limit is a non-blocking error and execution continues
  • async: true (command hooks only): fire-and-forget. Perfect for notifications and audit logs where you do not need the result. Not available for http, prompt, or agent handlers
  • allowedEnvVars: whitelist of env vars available for HTTP hook header interpolation. It does not scope env vars for command hooks
  • if field: only honored on tool-related events (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied)
  • disableAllHooks: true: emergency master kill switch. There is no per-hook disable — it’s all or nothing

Frequently Asked Questions

Q1: I configured a hook but it never fires. Where do I start?

Launch Claude Code with claude --debug and watch the event stream. Then open /hooks to confirm what is registered. The usual suspects are JSON validation errors in settings.json, broken matcher regex, and the fact that hooks are snapshotted at session start — if you edited settings mid-session, restart before testing again.

Q2: How do Hooks, Skills, Subagents, and MCP fit together?

Hooks are deterministic lifecycle interventions; Skills are reusable workflows the model invokes; Subagents are parallel/isolated tasks; MCP connects external tools. The comparison table above shows the split. My rule: safety and quality rules go in hooks, reusable playbooks in skills, soft preferences in CLAUDE.md.

Q3: Can a hook rewrite what Claude outputs?

Not directly. PreToolUse can rewrite tool inputs via updatedInput (useful for redacting secrets or fixing paths), and Stop hooks with prompt/agent handlers can block completion, but the model’s raw textual output is not mutable by hooks.

Q4: How do I force a hook across the whole organization?

Put the hook in Managed Policy and enable allowManagedHooksOnly. Users cannot override or disable Managed Policy hooks from their own settings.json, which makes this the only correct layer for compliance-critical rules.

Q5: What happens when a hook times out?

Default timeouts vary by handler — command = 600 seconds, prompt = 30 seconds, agent = 60 seconds. Exceeding the limit converts the hook into a non-blocking error and execution continues. Fire-and-forget hooks (audit logs, notifications) should always carry async: true, which is only available on command handlers.

Q6: Do Hooks work on the Free plan?

Claude Code is available on Pro, Max, Team, and Enterprise plans — anywhere a paid seat grants Claude Code access. Hooks require that underlying access. Once Claude Code is installed on a machine, hooks work in any project on it.

Q7: Do Hooks run on Windows and Linux?

Yes — macOS, Linux, and Windows (via WSL) are supported. For native Windows notifications you can also call PowerShell directly from a command hook, as shown in example 7.


Wrapping Up — Start With One Hook

Hooks are the one mechanism that turns CLAUDE.md requests into guarantees

In my team, Hooks landed in January 2026 and the format-diff and .env back-and-forths mostly died within a week, with review comments dropping roughly 30% by my count. Start with a single PostToolUse format hook in your own settings.local.json and grow from there.

Related reading on other parts of the Claude Code surface:

Canonical docs: Hooks reference and Automate workflows with hooks.

krona23

Author

krona23

Over 20 years in the IT industry, serving as Division Head and CTO at multiple companies running large-scale web services in Japan. Experienced across Windows, iOS, Android, and web development. Currently focused on AI-native transformation. At DevGENT, sharing practical guides on AI code editors, automation tools, and LLMs in three languages.

DevGENT about →

Leave a Reply

Trending

Discover more from DevGENT

Subscribe now to keep reading and get access to the full archive.

Continue reading