Hooks run your own shell commands at fixed points in Claude's lifecycle — before a tool, after an edit, when a session starts. Use them to format, guard, and automate deterministically.
Why: hooks let your code run at exact lifecycle moments, so behaviour is deterministic instead of "please remember to". When: pick the event for your goal — block a bad command before it runs, format a file after it is written, set up the environment when a session starts. Where: hooks are configured in settings.json.
PreToolUse — Before a tool runs. Can block it — use it to guard against dangerous commands.PostToolUse — After a tool succeeds. Use it to format, lint, or test edited files.UserPromptSubmit — When you submit a prompt. Can add context or reject the prompt.SessionStart / SessionEnd — At the start/end of a session — set up or tear down state.Stop / SubagentStop — When Claude (or a subagent) finishes responding.Flow of one tool call:
UserPromptSubmit ─► (model decides) ─► PreToolUse ─► [tool runs] ─► PostToolUseWhy: a PostToolUse hook formats files the instant Claude writes them, so the codebase stays clean without you asking. When: match the edit tools and run your formatter on the changed file. Where: hooks receive a JSON payload on stdin — pull the file path out with jq.
// .claude/settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs -r prettier --write"
}
]
}
]
}
}Why: a PreToolUse hook can inspect a command and stop it before it runs — a real guardrail, not a suggestion. When: exit with code 2 to block the action; the text on stderr is fed back to Claude so it adapts. Where: the hook reads the proposed tool input from stdin.
#!/usr/bin/env bash
# .claude/hooks/guard.sh — wired to PreToolUse with matcher "Bash"
cmd="$(jq -r '.tool_input.command')"
if echo "$cmd" | grep -qE 'rm -rf|git push --force'; then
echo "Blocked: '$cmd' is not allowed by policy." >&2
exit 2 # exit 2 blocks the tool; stderr goes back to Claude
fi
exit 0Why: hooks talk to Claude through a simple contract — JSON in, exit code (or JSON) out. When: read fields like tool_name and tool_input from stdin; signal the result with the exit code. Where: for richer control, print a JSON decision instead of relying on the exit code alone.
IN (stdin JSON): { session_id, hook_event_name, tool_name,
tool_input, cwd, ... }
OUT (exit code): 0 = allow / success
2 = block (PreToolUse) — stderr is shown to Claude
other = non-blocking error
OUT (advanced): print JSON, e.g.
{"hookSpecificOutput":{"permissionDecision":"deny",
"permissionDecisionReason":"..."}}