Hooks
Hooks are Claude Code's automation surface — deterministic bash (or any shell) scripts that fire on lifecycle events. Each hook receives JSON on stdin and returns JSON on stdout: {"decision": "approve"|"block", "reason"?: "..."}.
Events (matcher + input + output)
| Event | Fires | Input | Output shape |
|---|---|---|---|
PreToolUse |
Before any tool | {tool_name, tool_input} |
{decision, reason?} — block prevents the tool call |
PostToolUse |
After tool succeeds | {tool_name, tool_input, tool_output} |
{decision, reason?} |
PostToolUseFailure |
After tool fails | {tool_name, tool_input, error} |
{decision} — almost always approve |
Notification |
Claude needs input | {message} |
{decision} |
Stop |
Agent finishes turn | {stop_reason} |
{decision} |
UserPromptSubmit |
Prompt submitted | {prompt} |
{decision} + optional modified prompt |
SessionStart |
New session | {} |
{decision} — usually for context injection |
Installing a hook pack
Use MCP cc_kb_hook_recipe(name) to fetch a security-hardened pack. Each pack returns:
script— the bash script contentscript_path— target path (.claude/hooks/{name}.sh)settings_snippet— the JSON to merge into.claude/settings.jsonverify— one-line manual test
Available pack names (fetch a shortlist first via cc_docs_hook_pack_recommend(signals)):
protect-sensitive-files— block writes to .env/credentials (always recommended)auto-format-after-edit— prettier/black/rustfmt on Write|Editstop-until-tests-pass— block Stop if tests failpost-compact-context-restoration— re-load memory rules after /compactdirenv-reload-on-cwd-change— reload .envrc on cdtask-created-governance— log new tasks to memorytask-completed-quality-gate— enforce lint+test before task completionteammate-idle-enforcement— prevent idle teammate processes
Authoring a new hook
#!/bin/bash
set -euo pipefail
INPUT=$(head -c 65536)
# Validate JSON input
if ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then
echo '{"decision":"approve"}'; exit 0
fi
# Extract fields you need
FILE=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // ""')
# Validate path (prevent injection + traversal)
[ -z "$FILE" ] && { echo '{"decision":"approve"}'; exit 0; }
REAL=$(realpath "$FILE" 2>/dev/null) || { echo '{"decision":"approve"}'; exit 0; }
WD=$(realpath "$PWD")
[[ "$REAL" != "$WD"/* ]] && { echo '{"decision":"approve"}'; exit 0; }
BN=$(basename "$REAL")
[[ "$BN" == -* ]] && { echo '{"decision":"approve"}'; exit 0; }
# Do the work...
# Always emit valid JSON
echo '{"decision":"approve"}'
Safety rules:
- Always cap input with
head -c. - Always validate JSON before parsing.
- Always
realpath+ PWD-prefix check before touching a file. - Reject filenames starting with
-(flag injection). - Never
evalor unquoted-interpolate user content. - Default to
{"decision":"approve"}on any error — never block on bugs. - For concurrent writes to shared files (e.g. a log), use
flock.
Registering in settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/protect-sensitive-files.sh" }]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/auto-format-after-edit.sh" }]
}
]
}
}
Matchers are regex. Write|Edit fires for both tools. * fires for all tools.
Debugging
- Hook not firing: check
.claude/settings.jsonhas the event + matcher. Matcher regex is case-sensitive. - Hook blocks unexpectedly: check the hook's stderr output — Claude Code prints it.
- Hook slow: profile with
time bash .claude/hooks/.... Target <100ms for PreToolUse, <500ms for PostToolUse. - JSON parse error: run
bash .claude/hooks/x.sh < fixture.jsonlocally.
MCP delegation
| Need | Tool |
|---|---|
| Fetch a specific hook pack | cc_kb_hook_recipe(name) |
| Recommend packs from signals | cc_docs_hook_pack_recommend({has_formatter, has_tests, has_secrets, has_git, ...}) |
Anti-patterns
- Hooks that call LLMs → slow and non-deterministic. Hooks should be fast local computation.
- Hooks that
rm -rfor delete files → useBashtool with user confirmation, not a hook. - Hooks that swallow errors without emitting approve → hangs the session.
- Multiple hooks on same event that fight each other → run them sequentially in one script or use distinct matchers.
Reference
- hook-event-matrix.md — full event → input → output contract per event