Claude Code Hooks Library
Claude Code hooks are shell scripts that fire at fixed points in the agent loop — before a tool runs, after a file is edited, when a session starts, or when the agent tries to stop. In Learner Brain terms, they are the enforcement layer: a rule the agent keeps breaking graduates into a hook, and the correction never reaches you again. Every hook below is copy-ready.
The lifecycle events used in this library
SessionStartRuns when a session starts, resumes, or is cleared — inject standing context.UserPromptSubmitRuns on every user prompt — inspect it, log it, or add context before Claude reads it.PreToolUseRuns before a tool call — allow, deny, or ask. The strongest enforcement point.PostToolUseRuns after a tool call — validate the result and feed errors back into the loop.SubagentStopRuns when a subagent finishes — keep distilled reports distilled.StopRuns when Claude tries to finish — block the stop until quality gates pass.Claude Code also exposes Notification, PreCompact, and SessionEnd events — not used in this library, but worth knowing about.
Hooks run real commands
Filter by event
Filter by category
Showing 14 of 14 hooks
Block dangerous shell commands
PreToolUseBashDeny destructive commands — rm -rf, sudo, force-push, pipe-to-shell, and database drops — before they run.
When to use it
Give every Claude Code session a hard floor. This is the single highest-value guardrail: it converts your most important safety rules from suggestions into guarantees.
Notes
Deny reasons are fed back to Claude, so it learns what to do instead. Extend the pattern list to match your stack (e.g. add terraform destroy for infra repos).
Save as .claude/hooks/block-dangerous-bash.sh, make it executable, and register it under PreToolUse with matcher Bash in .claude/settings.json.
#!/usr/bin/env bash
INPUT="$(cat)"
COMMAND="$(echo "$INPUT" | jq -r '.tool_input.command // ""')"
if echo "$COMMAND" | grep -Eiq '(rm\s+-rf\s+(/|~|\$HOME)|sudo\s|git\s+push\s+.*--force|curl[^|]*\|\s*(ba)?sh|wget[^|]*\|\s*(ba)?sh|DROP\s+(TABLE|DATABASE)|TRUNCATE\s+TABLE|mkfs|dd\s+if=.*of=/dev/)'; then
jq -n --arg msg "Blocked destructive command: $COMMAND. Explain what you are trying to do and propose a safer alternative." '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": $msg } }'
exit 0
fi
jq -n '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow" } }'Block surprise package installs
PreToolUseBashDeny package install commands and ask Claude to justify the dependency before proceeding.
When to use it
Prevent Claude Code from adding dependencies without approval — keeps projects lean and avoids supply-chain surprises.
Notes
Good for keeping projects lean and preventing unnecessary dependency bloat. Pair with a stack-conventions rule that lists approved packages.
Save as .claude/hooks/block-package-installs.sh, make it executable, and register it under PreToolUse with matcher Bash in .claude/settings.json.
#!/usr/bin/env bash
INPUT="$(cat)"
COMMAND="$(echo "$INPUT" | jq -r '.tool_input.command // ""')"
if echo "$COMMAND" | grep -Eiq '(npm install|npm i |pnpm add|yarn add|bun add|pip install|uv add|composer require|cargo add|go get)'; then
jq -n --arg msg "Package install blocked for review: $COMMAND. Do not install packages automatically. Explain why the dependency is needed and ask for approval." '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": $msg } }'
exit 0
fi
jq -n '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow" } }'Protect .env and secrets
PreToolUseEdit|Write|MultiEditDeny any attempt to edit or create .env files, secret directories, or credential files.
When to use it
Enforce the 'never touch secrets' rule deterministically. The agent can read config; it cannot write credentials.
Notes
Reads are still allowed — only writes are blocked. Extend the path list with wherever your secrets live (e.g. k8s-secrets/, .vault/).
Save as .claude/hooks/protect-env-files.sh, make it executable, and register it under PreToolUse with matcher Edit|Write|MultiEdit in .claude/settings.json.
#!/usr/bin/env bash
INPUT="$(cat)"
FILE_PATH="$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')"
if echo "$FILE_PATH" | grep -Eiq '(^|/)(\.env|\.env\.|secrets?/|credentials|\.pem$|\.key$|id_rsa)'; then
jq -n --arg msg "Blocked write to protected path: $FILE_PATH. Secrets and environment files are managed manually. Ask the user instead." '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": $msg } }'
exit 0
fi
jq -n '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow" } }'Scan writes for hardcoded secrets
PreToolUseEdit|Write|MultiEditPattern-match the incoming edit content for API keys and tokens, and block the write if one is found.
When to use it
Catch the classic 'paste the API key into source' mistake before it ever hits disk — the mistake security reviews usually find after the commit is pushed.
Notes
Regexes cover common key formats (sk-, AKIA, ghp_, xox, JWTs). Expect occasional false positives; tune the patterns per stack.
Save as .claude/hooks/prevent-secrets-in-code.sh, make it executable, and register it under PreToolUse with matcher Edit|Write|MultiEdit in .claude/settings.json.
#!/usr/bin/env bash
INPUT="$(cat)"
CONTENT="$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // ""')"
if echo "$CONTENT" | grep -Eq '(sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{30,}|xox[baprs]-[A-Za-z0-9-]{10,}|eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,})'; then
jq -n --arg msg "Possible hardcoded secret detected in the edit. Block the write, show the offending line, and propose moving the value to an environment variable." '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": $msg } }'
exit 0
fi
jq -n '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow" } }'Block direct commits to main
PreToolUseBashDeny commits and pushes that target main or master — force feature branches and pull requests.
When to use it
Enforce your branching rule when the agent keeps committing straight to main despite the rule in CLAUDE.md.
Notes
This is the canonical 'rule graduates to a hook' example: the rule lived in CLAUDE.md, kept being violated, and became deterministic. Adjust branch names to your convention.
Save as .claude/hooks/block-commit-to-main.sh, make it executable, and register it under PreToolUse with matcher Bash in .claude/settings.json.
#!/usr/bin/env bash
INPUT="$(cat)"
COMMAND="$(echo "$INPUT" | jq -r '.tool_input.command // ""')"
if echo "$COMMAND" | grep -Eq '(git\s+(commit|push)).*(main|master)|git\s+push\s+(origin\s+)?(main|master)(\s|$)'; then
jq -n --arg msg "Direct commits/pushes to main are blocked. Create a feature branch, commit there, and open a pull request." '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": $msg } }'
exit 0
fi
jq -n '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow" } }'Run typecheck after edit
PostToolUseEdit|Write|MultiEditAfter edits to TS/TSX files, run the project's typecheck script and feed errors back to Claude.
When to use it
Catch type errors inside the agent loop instead of after it claims the task is done.
Notes
Can be slow on large codebases — enable when typecheck is fast enough for interactive coding. Feed the output back so Claude fixes its own mistakes immediately.
Save as .claude/hooks/typecheck-after-edit.sh, make it executable, and register it under PostToolUse with matcher Edit|Write|MultiEdit in .claude/settings.json.
#!/usr/bin/env bash
INPUT="$(cat)"
FILE_PATH="$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')"
# Only care about TypeScript files
echo "$FILE_PATH" | grep -Eq '\.(ts|tsx)$' || { echo '{}'; exit 0; }
# Only run when the project has a typecheck script
[ -f package.json ] && jq -e '.scripts["typecheck"]' package.json >/dev/null 2>&1 || { echo '{}'; exit 0; }
if ! npm run typecheck >/tmp/claude-typecheck.log 2>&1; then
REASON="Typecheck failed after editing $FILE_PATH. Fix the errors before continuing:\n$(tail -n 20 /tmp/claude-typecheck.log)"
jq -n --arg msg "$REASON" '{ "decision": "block", "reason": $msg }'
exit 0
fi
jq -n '{}'Run lint after edit
PostToolUseEdit|Write|MultiEditAfter edits to JS/TS files, run the project's lint script when it exists.
When to use it
Enforce your coding-standards rule deterministically — the agent loop fixes lint issues as it works instead of leaving them for you.
Notes
Noisy on projects with slow linters. Prefer lint-staged or targeted-file linting if your lint script sweeps the whole repo.
Save as .claude/hooks/lint-after-edit.sh, make it executable, and register it under PostToolUse with matcher Edit|Write|MultiEdit in .claude/settings.json.
#!/usr/bin/env bash
INPUT="$(cat)"
FILE_PATH="$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')"
echo "$FILE_PATH" | grep -Eq '\.(js|jsx|ts|tsx)$' || { echo '{}'; exit 0; }
[ -f package.json ] && jq -e '.scripts["lint"]' package.json >/dev/null 2>&1 || { echo '{}'; exit 0; }
if ! npm run lint >/tmp/claude-lint.log 2>&1; then
REASON="Lint failed after editing $FILE_PATH. Fix the reported issues before continuing:\n$(tail -n 20 /tmp/claude-lint.log)"
jq -n --arg msg "$REASON" '{ "decision": "block", "reason": $msg }'
exit 0
fi
jq -n '{}'Load Learner Brain context at session start
SessionStartInject a Learner Brain orientation reminder into every new session — the meta-learning rule, made unavoidable.
When to use it
Make the meta-learning rule load deterministically instead of hoping CLAUDE.md was read. Every session starts knowing it operates under the Upgrade Protocol.
Notes
Runs on startup, resume, and clear. Keep the injected text short — this is a pointer to the brain, not the brain itself. Edit the reminder to reference your actual rules folder.
Save as .claude/hooks/session-start-context.sh, make it executable, and register it under SessionStart in .claude/settings.json.
#!/usr/bin/env bash
# Runs on SessionStart (startup, resume, clear). Injects context into the session.
REMINDER="You operate under The Learner Brain methodology.
- Follow the rules in .claude/ and CLAUDE.md at all times.
- When you detect repetition, manual labor, or a preference correction, propose a Brain Upgrade (Observation / Suggestion / Benefit / Action) and wait for approval.
- Never store memory without explicit approval."
jq -n --arg ctx "$REMINDER" \
'{ "hookSpecificOutput": { "hookEventName": "SessionStart", "additionalContext": $ctx } }'Detect correction language, prompt an upgrade proposal
UserPromptSubmitScan each prompt for correction phrases — 'again', 'as I said', 'stop doing', 'like last time' — and remind Claude to propose a rule.
When to use it
This is the Unwritten Law Trigger made deterministic. When you correct Claude, the hook nudges it to codify the correction instead of just apologizing.
Notes
The hook adds context; it does not block. Claude still decides whether to propose — human approval still gates everything. Tune the phrase list to your own correction vocabulary.
Save as .claude/hooks/upgrade-proposal-reminder.sh, make it executable, and register it under UserPromptSubmit in .claude/settings.json.
#!/usr/bin/env bash
INPUT="$(cat)"
PROMPT="$(echo "$INPUT" | jq -r '.prompt // ""')"
if echo "$PROMPT" | grep -Eiq '(again|as i (said|told you)|like last time|stop doing|stop using|every time|i keep (saying|telling)|for the (third|3rd|fourth|4th) time|you keep)'; then
CONTEXT="Correction language detected in the user's prompt. Under the Learner Brain meta-learning rule: if this is a repeated correction or preference, propose a Brain Upgrade (Observation / Suggestion / Benefit / Action: Shall I create this rule?) before continuing the task."
jq -n --arg ctx "$CONTEXT" \
'{ "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": $ctx } }'
exit 0
fi
jq -n '{}'Log prompts for the next Brain Harvest
UserPromptSubmitAppend every user prompt to a dated friction log so the Harvest prompt can scan real sessions later.
When to use it
Build the raw material for Brain Harvests automatically. At the end of a sprint, run the Harvest prompt over the log to extract rules, skills, and error solutions.
Notes
Writes to .claude/friction-log/YYYY-MM-DD.log. Add the folder to .gitignore if prompts may contain sensitive content — or commit it if the repo is private and you want history.
Save as .claude/hooks/prompt-friction-log.sh, make it executable, and register it under UserPromptSubmit in .claude/settings.json.
#!/usr/bin/env bash
INPUT="$(cat)"
PROMPT="$(echo "$INPUT" | jq -r '.prompt // ""')"
LOG_DIR="${CLAUDE_PROJECT_DIR:-.}/.claude/friction-log"
mkdir -p "$LOG_DIR"
echo "$PROMPT" >> "$LOG_DIR/$(date +%F).log"
jq -n '{}'Block stop until tests pass
StopWhen Claude tries to finish, run the test suite and block the stop with the failures if tests fail.
When to use it
End the 'it said done, but the tests fail' pattern. The agent loop continues until the suite is green.
Notes
High risk of loop churn on flaky suites — scope it to fast unit tests, or gate it to a test:ci script. The block reason includes the tail of the test output so Claude knows what to fix.
Save as .claude/hooks/run-tests-before-stop.sh, make it executable, and register it under Stop in .claude/settings.json.
#!/usr/bin/env bash
# Stop hook: block the agent from finishing while tests fail.
[ -f package.json ] && jq -e '.scripts["test"]' package.json >/dev/null 2>&1 || { echo '{}'; exit 0; }
if ! npm test >/tmp/claude-tests.log 2>&1; then
REASON="Tests failed — you are not done. Fix the failing tests before stopping:\n$(tail -n 30 /tmp/claude-tests.log)"
jq -n --arg msg "$REASON" '{ "decision": "block", "reason": $msg }'
exit 0
fi
jq -n '{}'Notify when Claude finishes
StopFire a desktop notification when Claude Code finishes a task, with a reminder to review the diff.
When to use it
Step away while long tasks run. The notification doubles as a governance nudge: review the diff before committing.
Notes
Uses macOS osascript or Linux notify-send, whichever exists. Swap in your own command for Slack/Teams webhooks.
Save as .claude/hooks/notify-on-stop.sh, make it executable, and register it under Stop in .claude/settings.json.
#!/usr/bin/env bash
if command -v osascript >/dev/null 2>&1; then
osascript -e 'display notification "Claude Code task completed — review the diff before committing" with title "Claude Code"'
elif command -v notify-send >/dev/null 2>&1; then
notify-send "Claude Code" "Task completed — review the diff before committing"
fi
jq -n '{}'Keep subagents distilled
SubagentStopWhen a subagent finishes, remind the main thread that it received a distilled report, not raw output.
When to use it
Reinforce the Explore-subagent pattern: research tasks return structured summaries; the main context stays clean.
Notes
Pure reminder — adds context, blocks nothing. Useful on teams where subagents tend to dump full transcripts back into the main thread.
Save as .claude/hooks/subagent-stop-summary.sh, make it executable, and register it under SubagentStop in .claude/settings.json.
#!/usr/bin/env bash
CONTEXT="A subagent just finished. Treat its output as a distilled report: cite it, do not re-run its work, and keep the main context lean."
jq -n --arg ctx "$CONTEXT" \
'{ "hookSpecificOutput": { "hookEventName": "SubagentStop", "additionalContext": $ctx } }'Remind on multi-step prompts
UserPromptSubmitDetect multi-step language in a prompt and remind Claude to write a plan or todo list before starting.
When to use it
Enforce the 'plan before you code' rule on exactly the prompts that need it — refactors, migrations, anything with 'then' or 'after that'.
Notes
Reminder only; it does not block. Pairs well with a CLAUDE.md rule that requires a written plan for tasks over N steps.
Save as .claude/hooks/todo-discipline-check.sh, make it executable, and register it under UserPromptSubmit in .claude/settings.json.
#!/usr/bin/env bash
INPUT="$(cat)"
PROMPT="$(echo "$INPUT" | jq -r '.prompt // ""')"
if echo "$PROMPT" | grep -Eiq '(then|after that|first .* (then|and then)|step by step|migrate|refactor|across (all|every)|multiple files)'; then
CONTEXT="This looks like a multi-step task. Before writing code: state a short plan (or use TodoWrite), confirm the order of operations, and flag risky steps."
jq -n --arg ctx "$CONTEXT" \
'{ "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": $ctx } }'
exit 0
fi
jq -n '{}'Registering hooks
Claude Code hooks are wired up in .claude/settings.json (project) or ~/.claude/settings.json (global). Each event takes a list of entries;PreToolUse and PostToolUse entries take a matcher for the tool name:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-dangerous-bash.sh" }
]
},
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/protect-env-files.sh" }
]
}
],
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start-context.sh" }] }
]
}
}Make scripts executable with chmod +x .claude/hooks/*.sh. Scripts read a JSON payload from stdin (tool name, tool input, prompt, session info) and need jq installed. Deny decisions and block reasons are fed back to Claude, so it can correct course inside the loop.
Known limits & gotchas — Claude Code Hooks
Real-world quirks that bite if you don't know about them. Not deal-breakers — just things to design around.
- Hooks fire on every matching tool call — A PreToolUse matcher like Bash runs on every shell command. Keep scripts fast and exit early for irrelevant input, or every turn of the agent loop pays the cost.
- Blocking Stop can loop — A Stop hook that blocks with a reason sends Claude back to work. If the reason can't be fixed (flaky tests, environment issue), the loop churns. Scope stop-gates to fast, reliable checks.
- Settings precedence matters — Hooks can live in user, project, or local settings. Keep team hooks in .claude/settings.json (git-tracked) and personal experiments in settings.local.json so the brain stays shareable.
- jq and shell tools must be installed — These scripts assume jq, grep, and git are available. On minimal environments, install them first or the hook silently no-ops.
Universal Learner Brain Guides
These guides apply to all platforms — use your .claude/hooks/ path wherever they reference rule/skill locations.
Official Documentation
This template was last reviewed in September 2026 against the official Claude Code Hooks documentation. Spot something out of date? Let us know.