Claude Code works in a loop: it reads your prompt, decides on a step, calls a tool, reads the result, and decides again. Hooks let you attach your own command to named points in that loop. When the event fires, your command runs, every time, whether or not the model would have thought to do it.
That’s the point of hooks: deterministic control. An instruction in a prompt is a request the model usually follows; a hook is code that always runs. Use instructions for judgment, and hooks for rules.
Where they attach
Each hook listens for an event. The ones you’ll reach for first:
PreToolUse: before a tool runs. The only place you can stop a call outright.PostToolUse: after a tool succeeds, e.g. to run a formatter on a file Claude just edited.UserPromptSubmit: when you send a prompt, before Claude sees it.Stop: when Claude finishes responding, e.g. to send a desktop notification.
There are many more, covering sessions, subagents, compaction, permissions and file changes; the hooks reference lists them all.
How a hook decides
A command hook receives the event as JSON on stdin: which event fired, which tool, and its input. Its exit code is its answer. Exit 0 lets things proceed. Exit 2 is a blocking error: on PreToolUse it stops the tool call, and the reason the script wrote to stderr is what gets reported. For finer control, a hook can print JSON instead, for example a permissionDecision of allow, deny or ask.
Where they live
Hooks are configured in a settings file: ~/.claude/settings.json for you everywhere, .claude/settings.json for a project and its team, or .claude/settings.local.json for yourself in one project. Each entry names an event, a matcher that narrows which tools it applies to ("Bash", or "Edit|Write"), and the command to run. Plugins can ship hooks too.
The catch
A hook runs with your user’s permissions, so it can do anything you can. Keep hooks small, read any hook you didn’t write, and remember that a slow hook slows down every matching step.