Agent Hooks
Lifecycle hooks for agents
User-defined commands that the harness runs deterministically at specific points in an agent's lifecycle: before or after a tool call, at session start, when a response finishes.
You can tell an agent in its system prompt to "run the linter after every file edit." It usually complies; sometimes it forgets. Hooks turn "usually" into "always": you hand the rule to the harness running the agent instead of to the model. The short version: prompts and skills are suggestions, hooks are guarantees.
A hook is a command bound to an event in the harness lifecycle. The most commonly used events in Claude Code: - SessionStart — when a session opens (e.g. inject git status) - UserPromptSubmit — before the user's message reaches the model - PreToolUse — right before a tool runs; can block the call - PostToolUse — after a tool succeeds (format, lint, test) - Stop — when the model is about to finish; can say "not done yet"
The mechanics are simple: the harness writes the event payload (tool
name, input, session info) as JSON to the hook's stdin. A hook can
be a shell script, an HTTP endpoint, or a small model evaluation. It
answers through its exit code and stdout: exit 0 means
"proceed", exit 2 means "block", and whatever it writes to stderr is
fed back to the model. For finer control it returns JSON — for example
permissionDecision: "deny" on PreToolUse.
The model may not even know the hook exists; it only sees the outcome. That's why hooks are the most reliable way to make agent behaviour deterministic.
Like a safety interlock on a factory press. You train the operator to "keep your hands clear when the press comes down" (the prompt), but you also fit a two-handed button so the machine won't fire unless both hands are on it (the hook). Training guides behaviour; the interlock makes the accident physically impossible, however tired the operator is.
A team using Claude Code has two recurring problems: the agent
occasionally runs an overly broad rm -rf, and it sometimes forgets to
format the files it edits.
The fix is two hooks in .claude/settings.json:
1. PreToolUse with a Bash matcher: if the command contains
rm -rf, the script returns permissionDecision: "deny"; Claude
sees the reason and finds another way.
2. PostToolUse with an Edit|Write matcher: the formatter runs
automatically after every edit.
Neither rule depends on a prompt, on the model's attention in the moment, or on how full the context window is. And because the file is committed, everyone on the team gets the same protection.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write ."
}
]
}
]
}
}#!/bin/bash
# The harness writes the tool input as JSON to stdin
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q 'rm -rf'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "rm -rf blocked by hook"
}
}'
else
exit 0 # no decision; normal permission flow applies
fi- A rule must apply without exception (formatting, linting, never touching secrets)
- You want to block dangerous tool calls regardless of what the model decides
- Injecting dynamic context at session start (git status, open tickets)
- Running tests the moment the agent claims it's done (Stop)
- Side effects like audit logging or notifications
- The rule needs judgment and depends on context — that's a job for a prompt or a skill
- Heavy work taking seconds on every tool call — it slows the whole agent loop
- As your only security boundary — a hook is one layer, not a replacement for a sandbox
Treating silence as approval
A PreToolUse hook that exits 0 with no output doesn't approve anything; the call falls through to the normal permission flow. Blocking requires an explicit exit 2 or permissionDecision: "deny".
Trapping the Stop hook in a loop
A Stop hook that blocks every time keeps the agent from ever stopping. Check the stop_hook_active input field; Claude Code also force-ends the turn after 8 consecutive continuations.
Turning the hook into a vulnerability
A hook is a shell command running with your permissions. Quote variables, reject paths containing .., stay away from .env and key files, and reference scripts by absolute path via $CLAUDE_PROJECT_DIR.