There’s a category of things you want to happen every time, no matter what the agent decides — code gets formatted after an edit, a destructive command gets blocked, an action gets logged. Relying on the model to remember those is a losing game. Hooks are how you make them deterministic: shell commands that Claude Code runs automatically when a specific event fires.
This guide covers the events you can hook, where the config lives, and a set of working examples — auto-formatting, a guardrail that blocks risky commands, and activity logging. Hooks are one of the most underused features in Claude Code, and they’re the difference between an agent you have to babysit and one you can trust with a policy layer.
If you’re mapping out Claude Code’s features generally, this fits into the power-user guide alongside skills and subagents.
What a hook is
A hook ties an event to a command. When the event happens, Claude Code runs your command, passing details about what’s going on as JSON on standard input. Your command can read that, do something, and — for some events — influence what happens next.
The events you’ll use most:
Common hook events
| PreToolUse | Before a tool runs — can block it |
|---|---|
| PostToolUse | After a tool runs — react to the result |
| UserPromptSubmit | When you send a message |
| SessionStart | When a session begins |
| Stop | When the agent finishes responding |
PreToolUse is the one that makes hooks powerful, because it runs before the action and can refuse it. The rest are about reacting to things that already happened.
Where hooks live
Hooks go in your settings JSON. Project hooks belong in .claude/settings.json (committed, shared with the team); personal hooks go in your user settings under ~/.claude/. The structure groups hooks by event, with an optional matcher that limits which tools the hook applies to.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "npx prettier --write \"$CLAUDE_FILE_PATH\"" }
]
}
]
}
}
The matcher is matched against tool names, so this hook only fires after Edit or Write, not after every tool. The exact field names and the data passed on stdin can change between releases, so cross-check the hooks documentation when you build one.
Example 1: format code after every edit
The classic hook. Every time Claude edits or writes a file, run the formatter so you never get an unformatted diff:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "npx prettier --write \"$CLAUDE_FILE_PATH\"" }
]
}
]
}
}
Swap prettier for gofmt, black, rustfmt, or whatever your project uses. The win is that formatting stops being something the agent has to remember — it just happens, every time, on the file that changed.
Example 2: block a dangerous command
This is where PreToolUse earns its place. A hook can inspect a Bash command before it runs and refuse it. Point a small script at the tool call and exit non-zero to block:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": ".claude/hooks/block-dangerous.sh" }
]
}
]
}
}
#!/usr/bin/env bash
# Reads the tool call JSON on stdin, blocks rm -rf and force pushes.
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'rm -rf|git push --force|DROP TABLE'; then
echo "Blocked: refused a destructive command." >&2
exit 2
fi
exit 0
A non-zero exit stops the tool call. This is a guardrail, not airtight security — a determined agent or a cleverly worded command can still slip through patterns you didn’t anticipate — but it catches the obvious disasters before they run.
Example 3: log what the agent does
For an audit trail, append every Bash command the agent runs to a log file. Useful when you’re running Claude Code unattended or want to review a session afterward:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt" }
]
}
]
}
}
Every command the agent proposes gets recorded with no effort on your part. Pair it with a SessionStart hook that writes a timestamp and you have a readable history of what happened and when.
Example 4: load context when a session starts
A SessionStart hook can inject fresh context — the current git branch, open tickets, recent deploy status — so the agent begins each session informed:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "git status --short && git log --oneline -5" }
]
}
]
}
}
The output becomes available to the agent at the start of the session. It’s a lightweight way to keep Claude oriented without you typing the same status summary each time.
Windows and WSL notes
Hooks run shell commands, so the environment matters more here than with most Claude Code features.
- On native Windows, commands run through your Windows shell. Tools like
jqandgreparen’t there by default, so either install them or keep hooks to commands Windows has. - On WSL, hooks run as normal Linux commands, so the Unix tooling the examples use is available. For anything beyond a one-line formatter call, WSL is the smoother choice.
- Keep paths consistent with the environment — Windows paths in Windows, Linux paths in WSL. If you haven’t set up WSL, the install guide covers it.
When to use a hook vs a skill
Both shape behavior, but they’re not interchangeable.
Hook or skill?
| Use a hook when | It must happen every time, deterministically — formatting, guardrails, logging |
|---|---|
| Use a skill when | You want Claude to follow a procedure or convention when relevant |
| Hook strength | Runs regardless of what the model decides |
| Skill strength | Carries nuance and judgment the model applies in context |
A hook is a hard rule; a skill is guidance. Format-on-save is a hook. “Follow our commit message style” is a skill. The full comparison across extension points is in skills vs MCP vs subagents.
Setting up your first hooks
- Add a PostToolUse formatter hook matched to Edit|Write
- Add a PreToolUse guardrail that blocks destructive commands
- Test each hook script by hand before trusting it
- Commit project hooks in .claude/settings.json for the team
- Run jq-based hooks in WSL on Windows machines
Wrapping up
Hooks turn the things you want to happen every time into things that do. Start with a formatter on PostToolUse and a guardrail on PreToolUse — those two alone make Claude Code feel more trustworthy. Add logging and a SessionStart context loader as you find the manual steps you keep repeating.
For ready-made patterns worth copying, see the best Claude Code hook examples, and fit hooks into the bigger picture with the power-user guide.