Last issue ended owing a debt: "don't send an agent to do a deterministic job." The official definition of hooks has exactly one load-bearing clause — deterministic control: certain actions always happen rather than relying on the LLM to choose to run them. Models forget, skip, and decide "not this time." Hooks don't. We start from a Prettier config you can paste today, take apart the hook registry of the official security-guidance plugin, then name six community tools.
There is one question behind "should this be a hook": does it require judgment? If yes, give it to the model (prompt, skill, subagent). If no — same every time, and one miss hurts — pin it to a hook. "I wrote in CLAUDE.md that formatting must run after every edit" is not automation, it is prayer: it lands in the model's context, not in the execution path. Same order as always: configs you can copy first, mechanism second, named installables third, criteria in the back half.
The official guide gives this verbatim. Drop it into .claude/settings.json at the project root and Prettier runs every time Claude finishes an Edit or Write:
// .claude/settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
]
}
]
}
}
Event data arrives as JSON on stdin, which is why every official Bash example reaches for jq (brew install jq / apt-get install jq). When not to write this one: if the file can also be rewritten by a Bash command, the Edit|Write matcher misses it — the docs point you at a FileChanged hook watching the filename instead.
Save this as .claude/hooks/protect-files.sh (remember chmod +x) and register it on PreToolUse as "$CLAUDE_PROJECT_DIR"/.claude/hooks/protect-files.sh:
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# On a match, exit 2: the action is blocked, stderr goes back as feedback
case "$FILE_PATH" in
*.env|*/secrets/*) echo "Protected path, writes are denied" >&2; exit 2 ;;
esac
exit 0
/plugin install security-guidance@claude-plugins-official
# If the marketplace is not found, add it first:
/plugin marketplace add anthropics/claude-plugins-official
security-guidance is the one official plugin built entirely on hooks; once installed there is nothing to invoke. What it takes off your plate: injection, unsafe deserialization, dangerouslySetInnerHTML and friends move from "caught at PR review" to "caught as the line is written." When not to install it: it adds a model call on every turn that changed files and a deeper agentic review on every commit (Claude Opus 4.7 by default) — that is real money; outside a git repository the last two layers silently skip; cloud sessions do not load plugins at all. If the cost bites, keep only the free layer: ENABLE_CODE_SECURITY_REVIEW=0 turns off every model-backed review and leaves the pure string-match per-edit check.
The event table in the official reference now lists 33 entries. The "13 lifecycle events" you see in community write-ups is stale. A few that get overlooked and shouldn't:
command — run a shell command. Default timeout 10 minutes.http — POST to an endpoint; the same JSON comes back in the response body. $VAR in headers resolves only for names listed in allowedEnvVars.mcp_tool — call a tool on a configured MCP server directly.prompt — hand the event data to a model (Haiku by default) for a {"ok": true/false, "reason": "..."} verdict. 30-second timeout.agent — spawn a subagent that can read files and run commands before deciding. 60-second timeout, up to 50 tool turns. Marked experimental; the docs say prefer command for production.0 = success; stdout is parsed as JSON (and treated as plain text if it isn't, which some events inject into Claude's context). 2 = block, with stderr as the reason — but only on blockable events (PreToolUse, UserPromptSubmit, Stop, SubagentStop, PreModelSwitch and a few more); SessionStart and friends cannot be blocked, so exit 2 there merely shows stderr to you. Anything else = non-blocking error: the action proceeds and a hook error notice appears in the transcript.
/hooks browses every configured hook grouped by event — but it is read-only; adding and removing means editing the JSON. The global off switch is "disableAllHooks": true, and administrators have allowManagedHooksOnly, which disables your own hooks and runs only the managed ones.
security-guidance splits itself into five registrations. This table is worth memorizing as a template (the source is on GitHub: .claude-plugin/ + hooks/ + tests/):
Three things to steal outright: put the expensive layer last (free string matching per edit, paid model review on Stop and commit); baseline first, verdict later (UserPromptSubmit snapshots, Stop diffs); and the reviewer is not the instance that wrote the code (a separate call with a fresh context).
$CLAUDE_PROJECT_DIR does not follow you into a worktree. It always points at the project root where the session started, so "$CLAUDE_PROJECT_DIR"/.claude/hooks/x.sh keeps running the script in the main checkout — which is correct. To learn where Claude is actually working, read the cwd field in the hook's input JSON; that one does follow worktrees and cd.GowayLee/cchooks · Python SDK (132★)pip install cchooks # or: uv add cchooks
from cchooks import create_context, PreToolUseContext
c = create_context()
assert isinstance(c, PreToolUseContext)
if c.tool_name == "Write" and ".env" in c.tool_input.get("file_path", ""):
c.output.exit_deny("Nope! .env files are protected")
else:
c.output.exit_success()
Kills: hand-rolled json.load(sys.stdin) and memorized exit-code semantics; each event becomes a typed context object. Covers 9 events. When not to install: if your hook is a one-liner jq | xargs, a dependency is not worth it — and 9 events is far short of the current 33, so anything exotic goes back to raw parsing. (Per the GitHub repo page, 2026-09.)
syou6162/cchook · YAML instead of JSON (34★)go install github.com/syou6162/cchook@latest
PostToolUse:
- matcher: "Write|Edit"
conditions:
- type: file_extension
value: ".go"
actions:
- type: command
command: "gofmt -w {.tool_input.file_path}"
Kills: the escaping hell inside settings.json; conditions (by extension, by pattern) become declarative, and it ships a dry-run mode. When not to install: the moment a team shares config, a non-official format is one more thing newcomers cannot read — and the star count says it is still niche. (Per the GitHub repo page, 2026-09.)
johnlindquist/claude-hooks · TypeScript (393★)npx claude-hooks # generates .claude/hooks/index.ts; execution needs Bun
Kills: guessing at payload shapes — you get typed payloads with autocomplete, all events dispatched from one index.ts. When not to install: it binds your runtime to Bun, so on CI or a teammate's machine without Bun the hook fails silently — and a failing hook is non-blocking by default, so without debug logging you never find out. (Per the GitHub repo page, 2026-09.)
disler/claude-code-hooks-mastery · the teaching repo (3.9k★)This repo implements every event of its day as an Astral uv single-file Python script, including transcript backups before PreCompact and intercepting subagent completion. Kills: the question "what does this event's JSON actually look like" — reading it beats grepping the docs. When not to install: it is a reference, not a product; the README claims 13 events while the official reference is now at 33, and copying the whole .claude directory drags in its output styles and status line too. (Per the GitHub repo page, 2026-09.)
disler/claude-code-hooks-multi-agent-observability · a dashboard (1.5k★)It POSTs every event over HTTP to a Bun server that broadcasts over WebSocket to a Vue 3 client, so several concurrent agents become visible in real time. Kills: the blindness of "five agents are running and one is stuck." When not to install: it needs uv, Bun and a long-running server — overkill for one person in one session, and the official http hook type now POSTs to an endpoint natively, so wiring your own log service may be lighter than adopting this stack. (Per the GitHub repo page, 2026-09.)
pre-commit/pre-commit · the half that isn't inside Claude (15.6k★)pre-commit (pip install pre-commit → pre-commit install, configured in .pre-commit-config.yaml) governs the moment of committing. It is not a substitute for Claude's hooks, it is the next leg of the relay: Claude hooks only cover what Claude does, and commits you type in your own shell are invisible to them (the docs say even a ! shell escape does not count). When not to install: solo work where every change goes through Claude — then you are running the same linters twice.
command hook. Yes, but the event data is enough → prompt. Only by reading the code → agent (knowing it is experimental).PostToolUse. An incident (a committed secret) → block it in PreToolUse with exit 2, because PostToolUse cannot undo anything.hooks/hooks.json, skill frontmatter and subagent frontmatter all carry them, so installing a plugin means accepting everything it registers.claude --debug or /debug mid-session.if is best-effort, not a security boundary. A filter like if: "Bash(rm *)" runs the hook anyway when Claude Code cannot determine which commands the input runs. The docs say it plainly: hard allow/deny belongs to the permission system, not to a hook.Edit|Write never sees a file written by Bash. For real coverage the docs give three routes: a Stop hook scanning git status --porcelain once per turn; also matching Bash|PowerShell; or a FileChanged hook on the file itself.SessionEnd hooks share 1.5 seconds (raised to at most 60 only if you set a longer explicit timeout). Read that before planning cleanup or log uploads there.prompt and agent hook types invite back exactly the nondeterminism you just evicted. They are useful — judgment calls should be judged by a model — but be clear-eyed: a Stop hook where Haiku returns ok: true/false is not a deterministic guarantee, it is one more model question asked somewhere else. If you need "this always happens," only command delivers it.Install these three first: ① /plugin install security-guidance@claude-plugins-official — the one official plugin built entirely on hooks, zero invocation needed; if the cost bites, ENABLE_CODE_SECURITY_REVIEW=0 keeps the free layer. ② the Prettier PostToolUse config above (plus jq) — five lines of JSON that can go into .claude/settings.json today. ③ pre-commit (.pre-commit-config.yaml) — covers the half Claude's hooks cannot see: the commits you type yourself.
Read next, as needed: Hooks reference (all 33 events, five type schemas, exit-code semantics) · security-guidance source (how to launch a separate model call from a hook and feed the result back) · cchooks (Python, kills stdin boilerplate) · cchook (YAML config plus dry-run) · claude-hooks (typed TypeScript, needs Bun) · claude-code-hooks-mastery (what each event's JSON looks like, used as a dictionary) · multi-agent observability dashboard (live flow across concurrent agents).
Explicitly don't: use an if condition as a security boundary (that is the permission system's job) · expect Edit|Write to catch files written by Bash · use PostToolUse to stop what belonged in PreToolUse · install a plugin without checking which hooks it registers · do more than 1.5 seconds of work in SessionEnd · treat prompt/agent hooks as a determinism guarantee.
Edit|Write, mcp__.*__write.*); on SessionStart it is startup|resume|clear|compact|fork; on FileChanged it is literal filenames, not a regex.PreToolUse, UserPromptSubmit, Stop and others).additionalContext$CLAUDE_PROJECT_DIR vs cwdcd — use it to know where work is happening.{"ok": bool, "reason": str}. The agent type is marked experimental.disableAllHooksallowManagedHooksOnly: run only managed hooks and disable the user's own.prompt and agent types that depend on it?gofmt or asks Haiku is a separate question. So these two types are not a betrayal of the design; they split "always happens" from "always correct" — it is the users who tend to read them as one thing.security-guidance runs its review from a Stop hook. Can't that keep interrupting the model forever?Stop hook blocks too many times consecutively. Any hook that can tell the model to continue must carry its own iteration cap, or what you built is not automation but a perpetual motion machine assigning itself work./hooks lists every configured hook with its source file), and the reviewing is left to you. "Security is a shared responsibility" is not boilerplate disclaiming here — in this design the only party who can judge whether a given hook should run is the one who knows what is in their own project. Which makes running /hooks before installing a plugin, and again afterwards to see what appeared, the one gate in this system that genuinely belongs to you.SessionStart with a compact matcher to re-inject context, CwdChanged + FileChanged + CLAUDE_ENV_FILE, the config-location table, the prompt/agent/http types, limitations and troubleshooting (accessed 2026-09)command/http/mcp_tool/prompt/agent; full exit-code semantics for 0/2/other plus the list of blockable events; output fields such as permissionDecision, additionalContext and updatedInput; the Security considerations section; $CLAUDE_PROJECT_DIR behavior under worktrees (accessed 2026-09).claude/security-patterns.yaml rule schema, the five ENABLE_* switches, and the hook registry table under "How the plugin integrates with Claude Code" (accessed 2026-09).claude-plugin/ + hooks/ + tests/. Read it to see a hook launch an independent model call and feed the result back into the sessionpip install cchooks, typed context objects with exit_deny/exit_success, covering 9 events (132★ per the GitHub repo page, 2026-09)go install github.com/syou6162/cchook@latest, replaces settings.json escaping with YAML plus {.tool_input.file_path} templates, ships a dry-run mode (34★ per the GitHub repo page, 2026-09)npx claude-hooks generates .claude/hooks/index.ts, strongly typed payloads; execution needs Bun and the CLI needs Node 18+ (393★ per the GitHub repo page, 2026-09)uv single-file Python scripts; the README claims 13 events (the official reference is now 33, so mind the version gap) and includes tricks like backing up the transcript before PreCompact (3.9k★ per the GitHub repo page, 2026-09).pre-commit-config.yaml; it governs the moment of committing and covers the half Claude's hooks cannot see (15.6k★ per the GitHub repo page, 2026-09)