Claude Code Guide — Part 5: Hooks
Created: April 27, 2026 | Modified: May 3, 2026
What hooks fix
Part 4 fixed the context-overflow failure mode by giving you forked-context agents. This part fixes the other one: rules-as-prose drift.
You wrote "never write to .env" in CLAUDE.md. You wrote "always run lint after edits" in a Skill. The model honors both most of the time. Most of the time is not the same as every time. Prose instructions are advisory — the model reads them, weights them, sometimes overrides them on a tired Tuesday. Anything you actually want enforced needs a different surface.
A hook is a shell command Claude Code runs deterministically at a named lifecycle event. The harness fires the command, reads the exit code, and either lets the action proceed or blocks it. Exit 2 blocks. Exit 0 lets it through. The model is not in the loop.
Hooks are also the one part of .claude/settings.json you hand-author. The rest of the file builds itself as you click through permission prompts; the hooks stanza is the exception. The full schema, the event list, the payload shape, and any new events Anthropic ships after this article was written all live in the canonical reference (see Hooks in the official docs). Read it once before authoring; the article is short, the schema is precise, and there is no point reproducing it here.
How the surface fits together
Three concepts cover everything you actually need to think about. The docs page above teaches each one rigorously; the framing here is just enough to know which knobs you are turning.
The lifecycle event. A name like PreToolUse or PostToolUse that picks the moment your hook fires — before a tool call, after one, on prompt submit, on session start, on the agent returning. You pick exactly one event per hook, by name, in the hooks stanza. The event answers when.
The matcher. A regex tested against the tool name on PreToolUse and PostToolUse, ignored on the rest. Bash matches Bash calls; Write|Edit matches either; an empty string matches every call on those events. The matcher answers which calls.
The command and its exit code. A shell string Claude Code runs with the event payload on stdin. Exit 2 blocks the action and feeds your stderr back to the model. Exit 0 proceeds. Any other non-zero surfaces but does not block. The command answers what to do; the exit code answers whether to let the call through.
Past those three, the only other thing you reach for routinely is jq — to read fields like tool_name, tool_input.file_path, or tool_input.command out of the payload Claude Code feeds you on stdin. The exact field set lives in the docs.
Author your first hook
Two real hooks. The first runs after every Write or Edit and prints a short receipt to stderr so you can see the harness firing. The second runs before any Bash call and refuses any command that touches .env — the rule-as-prose example from the opening, now enforced.
The prompts panel walks you through both end-to-end: detect any existing settings, interview for the behaviour you want enforced, map intent to one event and one matcher, compose the shell command, merge into .claude/settings.json without clobbering the permissions block, and trigger the hook on a real call. Two steps in the panel — a decision interview, then the registration.
The receipt hook is a one-line jq script reading the file path off stdin and printing it to stderr. The block hook is a jq -e filter that exits non-zero when the Bash command contains .env, branches into exit 2, and otherwise falls through to exit 0. The exact strings are in the panel; the shape to internalise here is one line per intent. A useful first hook is small.
Open the prompts panel — right side on desktop, the floating button on mobile — and paste the first prompt to start.
Confirm the wiring
Registration is necessary, not sufficient. Trigger each hook deliberately on a real tool call so you can see it fire before you trust it. The receipt hook is verified by asking Claude Code to make a tiny edit and watching for the edited /full/path/to/file line in the session UI. The block hook is verified by asking it to run a Bash command that touches .env and watching the call get refused with your stderr message.
Both checks are the final phase of the second prompt in the panel. If a hook does not fire, the failure is almost always one of three things: jq is not on PATH, the JSON in settings.json does not parse (run jq . .claude/settings.json to confirm), or the matcher regex does not catch the tool name on the actual payload.
When to reach for a hook
Three patterns cover most real hook authorship.
Validation that has to happen. Lint after every edit. Type-check on every save. Tests after every commit. Any check you have ever forgotten to run manually is a candidate. A PostToolUse hook with a Write|Edit matcher running pnpm lint --filter affected is fifteen lines of JSON and removes a whole category of follow-up turn.
Refusals you cannot afford to leave to prose. The .env block above. Any Bash command pattern you treat as a hard line — rm -rf outside specific directories, anything that writes to /etc, anything that pipes a curl into a shell. Prose says "do not"; a hook makes it actually impossible.
End-of-session sanity. A Stop hook that prints the diff next to the brief, or runs the test suite one last time, or posts a summary somewhere. The lightweight version of CI that fires before you walk away from a session.
The pattern that earns hooks their place: anything you would forget to do, or anything you would override under pressure, belongs in a hook. The hook does not get tired and does not negotiate.
Wire the pointer in CLAUDE.md
The ## Skills, subagents, hooks (pointers) anchor Part 1 reserved is now fully authored. Replace the hooks placeholder you left in Part 4 with concrete lines:
## Skills, subagents, hooks (pointers)
- Skills live in `.claude/skills/`. See `house-style/` for the reference Skill that shapes prose, and `commit/` for the task Skill behind `/commit`.
- Subagents live in `.claude/agents/`. See `repo-researcher.md` for read-only research; delegate to it via the Agent tool.
- Hooks live in the `hooks` stanza of `.claude/settings.json`. The PreToolUse hook blocks Bash commands that touch `.env`; the PostToolUse hook prints an edit receipt to stderr.
That anchor is now load-bearing in the way Part 1 promised — every primitive in the on-disk workspace has a one-line pointer the next reader can follow to the artifact.
What just changed
The on-disk workspace is complete in the sense that nothing else needs to be authored before you can do real work in it.
CLAUDE.mdat the root, with the five anchors from Part 1, all of them now real..claude/settings.jsonwith the permissions Claude Code built for you through the prompt UI, plus ahooksblock you hand-authored that fires deterministically on real lifecycle events..claude/skills/with the reusable procedures from Part 3..claude/agents/with the forked-context workers from Part 4.
The two failure modes Part 3 surfaced are both fixed. Long Skills move to agents and stop flooding the main context. Rules you want enforced move to hooks and stop being advisory.
What is next
Part 6 — The built-in tool inventory catalogues the tools Claude Code already has on hand — Read, Write, Edit, Bash, TodoWrite, AskUserQuestion, and the rest — so you know what the model is reaching for when it surfaces a tool call in your session. Knowing the inventory is what makes the next phase, the four-phase method, concrete.