Claude Code hooks
Hooks run commands, handlers, or prompts around Claude Code lifecycle events. They help convert team rules into repeatable checks instead of relying on memory.
- Last verified
- Applies to
- Claude Code 2.1.x. CLI spellings were checked on local version 2.1.198; current official docs may describe newer 2.1.x behavior.
- Verification method
- Official documentation, CLI reference, and changelog reviewed on 2026-07-19. Hook schema and exit semantics were checked against official references. The standalone blocker script can be tested locally; this audit did not install a hook into user settings.
- Official sources
- Hooks guideHooks referenceChangelog
Reproducible practice
Block a dangerous database command with a project hook
Build and test a deterministic PreToolUse guard before relying on it in a Claude Code session.
Prerequisites
- A trusted test repository, jq, and a POSIX shell.
- A reviewed .claude/hooks directory committed only if the team accepts the policy.
- No production database credentials in the test environment.
Steps
Create the blocker script
Save this exact script as .claude/hooks/block-drop-table.sh. It reads the Bash command from hook JSON, blocks a conservative pattern, and explains the denial on stderr.
Step 1 #!/bin/sh COMMAND=$(jq -r '.tool_input.command // ""') if echo "$COMMAND" | grep -Eiq 'drop[[:space:]]+table'; then echo 'Blocked: DROP TABLE is not allowed' >&2 exit 2 fi exit 0Test the script outside Claude Code
A dangerous fixture must exit 2; a harmless fixture must exit 0.
Step 2 chmod +x .claude/hooks/block-drop-table.sh printf '%s' '{"tool_input":{"command":"drop table demo"}}' | .claude/hooks/block-drop-table.sh; test $? -eq 2 printf '%s' '{"tool_input":{"command":"git status"}}' | .claude/hooks/block-drop-table.shRegister a PreToolUse Bash matcher
Add a command hook in project settings only after JSON review. The hook command should reference $CLAUDE_PROJECT_DIR/.claude/hooks/block-drop-table.sh.
Exercise both paths in a disposable session
Request a harmless git status and a non-executing command string containing DROP TABLE; confirm the latter is denied before tool execution.
Expected result
The standalone fixtures return 0 and 2 as designed, and the session shows the denial reason without executing the matched Bash call.
Validation
- Parse the settings file with jq -e . .claude/settings.json.
- Test allow and deny fixtures directly after every script change.
- Use debug category hooks only when the hook does not fire as expected.
Failure handling
Hook never fires
Check event name, Bash matcher, executable bit, settings scope, and JSON parsing.
Everything is blocked
Print a redacted fixture to the script and narrow the pattern; keep fail-safe behavior explicit.
Cleanup or rollback
- Remove the hook entry first, then delete the script after confirming settings still parse.
- Keep a normal permission deny rule for high-risk actions; hooks should not be the only security boundary.
Boundaries and when not to use it
- Hook commands execute with the user's privileges. Treat reviewed hook code like production automation.
- Exit 2 blocks PreToolUse, but some other hook events cannot be blocked; event semantics are not interchangeable.
Lifecycle automation
Hooks can run at points such as session start, tool use, file changes, and stop events. This is useful for formatting after edits, checking command safety, or recording progress.
Quality gates
A hook can run lint, formatting, static checks, or custom validations after relevant actions. Keep hooks fast enough to support iteration and reserve heavier checks for deliberate verification steps.
Team conventions
Hooks can enforce repository-specific rules such as generated file restrictions, required review notes, or notification conventions. They should explain failures clearly so the agent and developer know what to fix.
Permissions
Pair hooks with permission rules for defense in depth.
Read guideCI/CD
Run the same hook checks locally and in GitHub Actions.
Read guidePlugins
Bundle team hooks inside installable plugins.
Read guideWorkflows
Use Stop hooks for final lint or test steps before handoff.
Read guideFailure handling
A useful hook fails with a focused message and a recovery path. Avoid broad scripts that produce noisy output and make it hard to identify the actual blocked action.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "pnpm lint --fix" }]
}
]
}
}Common hook events
Hooks can run around session lifecycle, tool use, and stop events. Match the event to the check you want to enforce.
SessionStart
Load environment checks, notify the team channel, or print repository-specific reminders.
Read guidePreToolUse / PostToolUse
Validate or format around file edits, shell commands, and other tool calls.
Read guideStop
Run final lint, test, or summary steps before a session ends.
Read guideUserPromptSubmit
Inject guardrails or required context before Claude processes a new user message.
Read guideHook checklist
Keep hooks focused
A hook should validate one clear concern rather than run a broad, noisy project script.
Read guideMake failures actionable
Failure output should name the blocked action, the reason, and the next command or file to inspect.
Read guideBalance speed and safety
Run lightweight checks often and reserve slow checks for explicit verification or release steps.
Read guideVersion-control hook config
Store team hooks in the repository so every developer and CI environment uses the same rules.
Read guide