Claude Code hooks, with the missing manual.
Eight hooks we run on production client work: a secret shield, a danger blocker, lockfile and dev-server guards, a typecheck gate that makes Claude fix its own type errors, and more. Plus the reference the searches keep asking for: every event, the exact stdin JSON, exit codes, and blocking patterns.
Eight hooks that hold the line.
| Hook | Event | What it does |
|---|---|---|
| secret-shield | PreToolUse | Blocks commands and file writes containing API keys, tokens, private keys, credentialed URLs |
| danger-blocker | PreToolUse | Blocks rm -rf /, force-push to main, DROP DATABASE, disk-level writes, fork bombs |
| pnpm-guard | PreToolUse | Reads the lockfile, rejects the wrong package manager with the right one named |
| dev-server-guard | PreToolUse | Stops the agent launching npm run dev in its own shell |
| coauthor-scrubber | PreToolUse | Rejects commits carrying the Co-Authored-By trailer; Claude retries clean |
| typecheck-gate | Stop | Runs tsc --noEmit at turn end; errors bounce back to Claude to fix |
| format-on-write | PostToolUse | Prettier on each touched file, only in repos that use Prettier |
| turn-timer | UserPromptSubmit + Stop | Shows how long each response took |
Ships as a ready-to-enable plugin, and every script also works standalone with a five-line settings.json entry. Plain Node, no dependencies, readable in one sitting.
Why hooks beat instructions.
You can tell Claude “never use npm here” in CLAUDE.md, and it will comply, usually. A hook removes the usually: it is a script the harness itself runs around every tool call, and when it says deny, the command does not execute. Instructions are advice; hooks are law.
The elegant part is what happens after a block. The hook’s reason is fed straight back into Claude’s context, so a good reason (“this repo uses pnpm, found pnpm-lock.yaml”) means the model immediately retries the right way. You are not fighting the agent; you are giving it guardrails it can read.
The events, and when they fire.
| Event | Fires | Typical use |
|---|---|---|
PreToolUse | Before every tool call | Guardrails: allow, deny, or ask, per call |
PostToolUse | After a tool call succeeds | Formatters, linters, notifications |
UserPromptSubmit | When you send a prompt | Inject context (stdout becomes context), stamp timers |
Stop | When Claude finishes a turn | Quality gates: block the stop until checks pass |
SessionStart / SessionEnd | Once per session | Load state, write session logs |
SubagentStart / SubagentStop | Around subagent runs | Track or restrict delegated work |
PermissionRequest | On a permission prompt | Auto-answer known-safe prompts |
FileChanged / ConfigChange | Async, on changes | Watchers, cache busting |
Register hooks in settings.json under the event name, with a matcher against the tool (exact name, regex, or *):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node /path/to/danger-blocker.js" }
]
}
]
}
}The stdin/stdout contract.
Every hook reads one JSON object on stdin and may answer with JSON on stdout. For PreToolUse, tool_input carries the exact arguments about to run, and your verdict goes back as a permissionDecision:
// stdin (what your script receives):
{
"session_id": "abc123",
"cwd": "/repo",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "npm install lodash" }
}
// stdout (how you block it):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "This repo uses pnpm (found pnpm-lock.yaml).
Re-run with pnpm so the lockfile stays consistent."
}
}Exit codes are the shorthand alternative: exit 0 processes your stdout, exit 2 blocks the action with stderr fed back to Claude, anything else is a non-blocking warning. Stop hooks use a top-level decision instead:
// A Stop hook that refuses to let the turn end with type errors:
{
"decision": "block",
"reason": "tsc --noEmit reports 3 errors after your changes.
Fix them before finishing: src/auth.ts(42,7): error TS2339..."
}One warning from production: a Stop hook that blocks must check the stop_hook_active flag on its input, or a persistent failure loops forever. The typecheck gate in this pack shows the guard.
Common questions.
01What are hooks in Claude Code?
Hooks are scripts Claude Code runs at fixed points in its loop: before a tool call (PreToolUse), after one (PostToolUse), when you submit a prompt, when a turn ends (Stop), and a dozen more events. Unlike instructions in CLAUDE.md, which the model can overlook, hooks are enforced by the harness itself: a hook that denies a command means the command does not run, every time.
02How do I set up hooks in Claude Code?
Two ways. Add a hooks object to ~/.claude/settings.json (or .claude/settings.json in a repo for your whole team) mapping an event and a tool matcher to your script. Or install hooks as a plugin, which is what this pack ships: copy the folder, register it in settings, restart. The /hooks command in any session shows exactly what is active.
03How does a hook block an action?
A PreToolUse hook answers on stdout with a permissionDecision of deny (plus a reason), or simply exits with code 2 with the reason on stderr. The reason is fed back to Claude, which is the clever part: tell the model why it was blocked ("this repo uses pnpm") and it retries correctly on its own. A Stop hook can return decision: block to send Claude back to work, which is how the typecheck gate in this pack makes it fix its own type errors.
04What JSON does a hook receive?
Every hook gets a JSON object on stdin with session_id, cwd, transcript_path, hook_event_name, and permission_mode. Tool hooks add tool_name and tool_input (the exact arguments: the Bash command string, the file path and content for Write). That last field is what makes real guardrails possible: you inspect precisely what is about to run.
05Do these hooks work on Windows?
Yes. All eight are plain Node scripts with zero dependencies, and Node is already there because Claude Code runs on it. Tested on Windows, macOS, and Linux; no bash-isms, no jq.
06Can I use just one hook from the pack instead of all eight?
Yes. Every script is standalone. Copy the one file you want anywhere and wire it in settings.json with a five-line hooks entry (the README shows the exact snippet), or install the plugin and delete the entries you do not want from hooks/hooks.json.
- Claude Code skills packTen ready-made skills, plus the install and activation guide people actually search for.Free · 10 skills
- Claude Code subagents packSix specialist subagents, with the frontmatter reference and delegation patterns explained.Free · 6 subagents
- CLAUDE.md templatesFive battle-tested CLAUDE.md files: Next.js, Python, monorepo, API service, and a lean starter.Free · 5 templates
- Claude Code statuslineA one-file statusline showing model, git branch, context left and session cost. Paste, done.Free · 1 script
Want guardrails designed for your codebase?
These eight are the general-purpose set. The interesting ones are specific: hooks that know your deploy pipeline, your data boundaries, your compliance rules. That is the work we do for clients. techpotions is a small studio; you talk to the people who build.