Short answer
Claude Code hooks are scripts that run at fixed points in the agent's loop. A PreToolUse hook can deny a command, a PostToolUse hook can format or check each edit, and a Stop hook can refuse to let the agent finish until tests pass or a reviewer accepts. Together they turn an assistant you supervise into a pipeline that checks itself.
Most people use Claude Code as an assistant. You ask, it works, it stops, you read what it did and decide whether to ask again. That works until you notice that you are the loop: every check, every “now run the tests”, every “you missed a file” goes through you.
Hooks are how you take yourself out of that loop. They are small scripts Claude Code runs at fixed moments, and some of them can say no. Once a script can refuse to let the agent stop, an assistant starts to behave like a pipeline.
What Claude Code hooks are
A hook is an entry in a settings file that tells Claude Code: when this event happens, run this. The current docs list 33 events. The ones you will use most are few:
SessionStartfires when a session begins or resumes. Good for loading context.UserPromptSubmitfires before Claude sees a prompt.PreToolUsefires before a tool call runs, and can allow, deny or ask.PostToolUsefires after a tool call succeeds.Stopfires when Claude finishes responding, and can send it back to work.SubagentStopandTaskCompleteddo the same for subagents and tasks.
A hook can be a shell command, an HTTP call, an MCP tool, a single prompt to a Claude model, or (experimentally) a subagent that reads files before deciding. Command hooks get the event as JSON on stdin and answer with an exit code and optional JSON on stdout. Exit 0 means fine. Exit 2 means block, and whatever you wrote to stderr becomes the reason Claude sees. Any other exit code is a non-blocking error.
Hooks live in ~/.claude/settings.json for all your projects, .claude/settings.json for one project (committed), .claude/settings.local.json (not committed), managed policy settings, plugins, or skill and subagent frontmatter. The levels merge.
The hook lifecycle that makes a pipeline
The pipeline is not a single hook. It is a few hooks placed at the points where a person would otherwise step in.
Where hooks replace a person in one Claude Code session
- Automated check
- Agent
- Person
Steps as text
| Step | What happens |
|---|---|
| 1. SessionStart | Step — load ticket and criteria |
| 2. PreToolUse | Automated check — deny risky commands |
| 3. Claude works | Agent — edits and runs tools |
| 4. PostToolUse | Automated check — format, lint each edit |
| 5. Stop | Automated check — tests, then a reviewer |
| 6. Done | Person — or parked for a person |
| Loop | block: keep working: back from step 5 to step 3 |
The loop at the end is the important part. A Stop hook that returns "decision": "block" with a reason stops Claude from stopping, and the reason becomes its next instruction. That one mechanism is what kills “Would you like me to continue?”, a habit I take apart in why coding agents stop halfway.
Five Claude Code hooks examples worth copying
These are general patterns, simplified so they fit on a screen. Test each one on a throwaway branch first.
1. Deny destructive commands before they run
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-rm.sh" }
]
}
]
}
}
The script inspects tool_input.command and, for anything outside the build folder, prints a JSON answer with permissionDecision set to deny and a reason. Claude reads the reason and picks another route. The if field uses permission rule syntax, so the script only starts for rm commands instead of every shell call.
2. Format and check every edit
A PostToolUse hook matched on Edit|Write runs your formatter and a fast linter on the touched file. If the linter fails, exit 2 and print the errors. The edit already happened, so this doesn’t undo it; it puts the errors in front of Claude while the file is still fresh in its context. Keep it fast. A hook on every edit that takes ten seconds adds up across a long run.
3. Gate Stop on the test suite
#!/bin/bash
# .claude/hooks/stop-gate.sh
input=$(cat)
rounds_file="${CLAUDE_PROJECT_DIR}/.claude/.stop-rounds"
rounds=$(cat "$rounds_file" 2>/dev/null || echo 0)
if ! npm test > /tmp/stop-gate.log 2>&1; then
if [ "$rounds" -ge 3 ]; then
echo 0 > "$rounds_file"
exit 0 # give up and let a person look
fi
echo $((rounds + 1)) > "$rounds_file"
echo "Tests fail. Fix them before you stop:" >&2
tail -n 40 /tmp/stop-gate.log >&2
exit 2
fi
echo 0 > "$rounds_file"
The counter matters. The input includes stop_hook_active, which is true when Claude is already continuing because of a stop hook, and Claude Code itself caps consecutive continuations at eight. Your own limit should be lower and should end in a state a person can see. If you only need “keep going until X”, the built-in /goal command is a shortcut for a prompt-based Stop hook.
4. Hand the diff to a second model
The strongest Stop hook asks someone else. A command hook can write the diff to a file and call another agent in read-only mode, for example codex exec, which runs in a read-only sandbox by default:
git diff > .review/diff.patch
verdict=$(codex exec "Read .review/diff.patch and ACCEPTANCE.md. \
Reply ACCEPT, or list concrete findings with file and line.")
if ! printf '%s' "$verdict" | head -n 1 | grep -q '^ACCEPT'; then
printf '%s\n' "$verdict" >&2
exit 2
fi
Findings go straight back to Claude as the reason it can’t stop yet. Pair it with the round counter above. Why the reviewer should be a different model is covered in Claude Code vs Codex.
5. Load the task at session start
A SessionStart hook can print the ticket, the acceptance criteria and the current branch, and Claude Code adds that to context before the first prompt. Agents started by a script then begin with the same facts a person would have pasted in.
Which hooks to add first
Not every hook earns its place. This is how I’d rank the common ones by setup effort and by how much supervision they remove.
Which Claude Code hooks to add first
Data as a table
| Item | Effort to set up | Supervision it removes |
|---|---|---|
| Tests gate on Stop | Low | A lot |
| Deny risky commands | Low | A lot |
| Load task on start | Low | A lot |
| Second model review | High | A lot |
| Lint after each edit | Low | Little |
| Desktop notification | Low | Little |
The placements are my judgement, not measurements. The pattern holds, though: the hooks that decide when work is finished pay off the most, because “finished” is the question a person otherwise answers dozens of times a day.
Hook mistakes that break a pipeline
Bad: a Stop hook with no exit. A gate that can block forever turns a stuck agent into a burning budget. Always count rounds and end in a visible “parked” state.
Bad: trusting the agent’s word inside the hook. A hook that greps Claude’s last message for “all tests pass” checks a claim. Run the command and read the exit code yourself; I go into this in evidence over claims.
Bad: hooks the agent can edit. If .claude/settings.json and the hook scripts are writable by the agent, a determined run can weaken its own gate. Keep gates in managed or user settings, or deny edits to those paths with a PreToolUse hook.
Bad: slow hooks on hot events. Command hooks default to a 600-second timeout, and UserPromptSubmit to 30. A slow hook on every tool call adds up across a long run. Use async: true for hooks that only log, and remember async hooks can’t block.
Bad: running claude -p over a repository you didn’t write. In -p and SDK sessions Claude Code skips the workspace trust dialog, so hooks committed in that repo’s .claude/settings.json will run. Review them first, or pass --settings '{"disableAllHooks": true}'.
Good: hooks that keep context small. The cost docs suggest using hooks to filter test output or logs before Claude reads them, so the agent sees the 40 lines that matter instead of 10,000.
Good: hooks that are boring scripts in the repo, reviewed like any other code, with the gate logic outside the agent’s reach. A pipeline is only as trustworthy as the part the agent can’t change, which is the theme of guardrails for AI coding agents.
Where hooks end and a pipeline begins
Hooks give you control points inside one session. A full pipeline also needs things a session can’t hold: a queue of tickets, a place where parked runs wait for a person, a record of what each run cost and proved, and a way to run overnight without a terminal open. You can build those around Claude Code with the same pieces: claude -p started by a scheduler, hooks as the gates, and a second agent as the reviewer.
Start small. One Stop hook that runs your tests and counts rounds will change how you work within a day, because you will stop reading half-finished summaries. Add the reviewer when the tests stop being the thing that catches problems. Add a queue when you notice you are the one feeding tickets in. Each step removes one more moment where the agent waits for you.
Questions
What are hooks in Claude Code?
Hooks are scripts or prompts that Claude Code runs at fixed events such as PreToolUse, PostToolUse and Stop. They are configured in settings files and can allow, block or add context to what the agent does.
How do I stop Claude Code from stopping before the work is done?
Add a Stop hook that runs your checks and returns "decision": "block" with a reason, or exits with code 2 and writes the reason to stderr. Count rounds in the hook so it gives up after a few tries and hands the run to a person.
Can a Claude Code hook block a command?
Yes. A PreToolUse hook can return permissionDecision: "deny" with a reason, and Claude will not run the tool call. Exit code 2 also blocks it.
Are Claude Code hooks safe?
Command hooks run with your full user permissions, so treat them like any script you execute. Review hooks in repositories you didn’t write, especially before running claude -p, which skips the workspace trust dialog.