.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? |
|---|---|---|---|
command | Shell script / external binary | Validation, auto-format, logging | Yes (exit 2) |
http | HTTP POST request | Audit logs, SIEM integration | Yes |
prompt | Sub-prompt to Claude | LLM-driven judgment gates | Yes |
agent | Spawned sub-agent | Multi-turn review, workflow gates | Yes |
📑Table of Contents
- What Claude Hooks Actually Are — and Why CLAUDE.md Is Not Enough
- Hooks vs Skills vs CLAUDE.md vs MCP — When to Use Which
- Complete Claude Hooks Event Reference (April 2026)
- The Hook I/O Contract: stdin, Exit Codes, and JSON Output
- Eight Claude Hooks You Can Copy Today
- Team Rollout Playbook: Phase 1 → 3
- When Your Hook Doesn’t Fire — Debugging Checklist
- Security Checklist You Must Read
- Execution Order, Timeouts, and Concurrency
- Frequently Asked Questions
- 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 layeredsettings.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
.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 runs | Security enforcement, auto-format, audit logging, destructive-command blocks | Everything |
| CLAUDE.md | △ Model discretion | Tone, style guides, review criteria, project context | Hooks (complementary) |
| Skills | △ Model invokes | Reusable workflows (PR creation, release notes, debugging playbooks) | Called from hooks |
| Slash Commands | ○ User-triggered | Human-initiated routines | Pair with skills |
| Subagents | △ Model delegates | Parallel research, isolated tasks | Launched from agent-type hooks |
| MCP Servers | ○ Tool provider | External DBs, APIs, internal services | Matcher 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 |
|---|---|---|
SessionStart | Yes | New session. Inject context, run env checks |
SessionEnd | No | Session wrap-up. Save logs, clean up |
UserPromptSubmit | Yes | User message submitted. Prompt gating |
PreToolUse | Yes | Just before a tool call. Deny or rewrite input |
PostToolUse | No | Right after a tool call. Auto-format, audit |
Notification | No | Permission prompts / idle waits. Desktop notify |
Stop | Yes | Claude is about to finish. Gate completion |
SubagentStart/Stop | Stop only | Sub-agent lifecycle |
PreCompact | No | Before context compaction. Preserve state |
PermissionRequest | Yes | Permission 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 completesettings.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.
{
"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”:
- Open
/hooks— the browser is read-only, so you will edit the file directly, but the view confirms what Claude thinks is registered - Launch with
claude --debugand watch the event stream. Matching yourmatcherregex against the actual event name catches half the bugs - Validate
settings.jsonas JSON. A single trailing comma silently disables every hook in the file - Test the regex against the tool names you expect. Broad matchers like
Writehit a lot more than you might think - Restart the session. Hooks are snapshotted at session start — editing mid-session does nothing until you restart
- 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.envfrom 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
httphooks, pin destinations and useallowedEnvVarsto scope credentials
Execution Order, Timeouts, and Concurrency
- Multiple hooks on one event: run in registration order. For PreToolUse, any
denyhalts 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 forhttp,prompt, oragenthandlersallowedEnvVars: whitelist of env vars available for HTTP hook header interpolation. It does not scope env vars forcommandhooksiffield: 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
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:
- Claude Code: 15 Efficiency Techniques — everyday settings and shortcuts that pair well with hooks
- Claude Code Security Settings Guide — the permission and sandbox story that sits alongside hooks
- Best Claude Plugins: 3 Essentials — when to reach for a plugin or skill instead of a hook
- Claude Can Now Control Your PC — Computer Use paired with hook-based guardrails
Canonical docs: Hooks reference and Automate workflows with hooks.
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.


![Harden Claude Code CLI: 9 Proven Steps for Business Use [2026]](https://i0.wp.com/devgent.org/wp-content/uploads/2026/03/claude-code-security-eyecatch.webp?fit=300%2C167&ssl=1)






Leave a Reply