Hooks are shell scripts that Claude Code runs automatically at key moments — no prompting required. They give you persistent guardrails, quality gates, and automation that work across every session.
Hooks are configured in .claude/settings.json under the "hooks" key:
{
"hooks": {
"SessionStart": [...],
"PreToolUse": [...],
"PostToolUse": [...],
"Stop": [...]
}
}Each hook entry specifies a shell command to run. The hook can:
- Allow the action by exiting with code 0
- Block the action by exiting with non-zero code (PreToolUse only)
| Type | When It Fires | Can Block? |
|---|---|---|
SessionStart |
When a Claude session opens | No |
PreToolUse |
Before a tool runs | Yes |
PostToolUse |
After a tool completes | No |
Stop |
When a session ends | No |
Fires: When you open claude in this directory.
What it does:
- Prints the current date/time
- Shows current git branch and last 5 commits
- Reads and displays
.estado.mdif it exists (previous session summary) - Warns if
.envis missing or has unfilled placeholders - Lists all available skills
Why it matters: You never start a session blind. Claude always knows where the project is.
Customize: Edit .claude/hooks/session-start.sh to add project-specific checks.
Fires: Before every Bash tool execution.
What it does:
- Logs every command to
.claude/audit.logwith a timestamp - Blocks commands matching dangerous patterns:
rm -rf /orrm -rf ~- Fork bombs (
:(){:|:&};:) - Disk wipes (
dd if=/dev/zero,mkfs.) DROP TABLE,DROP DATABASE,TRUNCATE TABLE- Force-pushes to
mainormaster
- Warns (but allows) on:
pip install,npm install(review before running)curl | bashpatterns
Why it matters: A single safety net catches the most catastrophic mistakes.
Customize: Edit BLOCKED_PATTERNS and WARN_PATTERNS arrays in the script.
Important: This block-list is defense in depth, not the primary safety boundary. See Hooks vs Claude Code's Sandbox for what it can and cannot catch.
Fires: Before every Bash tool execution (filters internally — only acts on git commit).
What it does:
- Inspects
git diff --stagedfor likely secrets (api keys, tokens, passwords). Blocks the commit if any are found. - Warns on debug statements (
console.log,print(,debugger,pdb.set_trace,breakpoint(),TODO: REMOVE,FIXME: REMOVE). - Warns on staged files larger than 1MB.
- Runs
flake8on staged.pyfiles (warning only). - Runs
npx eslinton staged.js/.jsx/.ts/.tsxfiles (warning only).
Why it matters: Replaces the deterministic checks of the old /pre-commit skill so
they fire automatically before every commit — no need to remember to invoke a skill.
Customize: Edit .claude/hooks/pre-commit-check.sh. The LLM-judgment portion of the old
skill (Conventional Commits message generation) now lives in
skills/gen-commit-message.md.
Fires: After every Edit or Write tool call.
What it does:
- Detects the file extension of the modified file
- For
.pyfiles: finds the nearesttests/directory and runspytest - For
.js/.tsfiles: finds the nearestpackage.jsonand runsnpm test - Reports pass/fail back to Claude
Why it matters: Claude immediately knows if an edit broke tests — without you having to ask.
Customize: Add more file types or change the test command in the script.
Fires: When the Claude session ends.
What it does:
- Reads the session summary from
CLAUDE_STOP_HOOK_SUMMARYenvironment variable - Prepends the summary to
.estado.md(most recent first) - Stages
.estado.mdfor git (but does not commit)
Why it matters: Session state persists across restarts. The next session's session-start.sh reads it.
Every bash command Claude runs is logged to .claude/audit.log:
[2026-03-13 14:22:05] BASH: python -m pytest tests/ -v
[2026-03-13 14:22:11] BASH: git diff --staged
This log is in .gitignore — it's local only. Use it to review what Claude did in a session.
ClaudeMaxPower ships a hook self-test you can run anytime:
bash scripts/test-hooks.shIt runs each hook script with synthetic env vars in an isolated temporary workspace and asserts the expected behaviour:
pre-tool-use.shallows benign commands and blocksrm -rf /and force-pushes to mainpre-commit-check.shexits 0 silently for non-git commitcommands; blocks on staged secretspost-tool-use.shexits 0 when the file path is empty or non-sourcestop.shwrites a session entry to.estado.md(inside the tmp workspace, never your real one)session-start.shruns cleanly even outside a git repository
A mutation guard at the end confirms git status is unchanged after the script
runs. If you customise a hook, re-run this script to verify your change still
respects the contract Claude Code expects.
What it does NOT verify: whether Claude Code itself fires the hooks at the right moment. That requires running inside Claude Code with appropriate tracing. The self-test verifies the scripts are correct; firing is Claude Code's job.
ClaudeMaxPower hooks are defense in depth, not the primary safety boundary. The primary boundary is Claude Code itself — its per-tool permission prompts, its permission mode, and any sandbox flags Claude Code exposes. Hooks are a thin backstop that catches a handful of obviously catastrophic patterns; they do not replace Claude Code's own enforcement.
| Layer | Enforces | Examples |
|---|---|---|
| Claude Code (primary) | Per-tool permission prompts; the active permission mode (acceptEdits, auto, bypassPermissions, plan); MCP server permissions; the harness's tool-call surface |
Asks before running unfamiliar Bash commands; refuses unauthorized file writes in plan mode; mediates MCP tool access |
| ClaudeMaxPower hooks (backstop) | A small regex block-list in pre-tool-use.sh; auto-test on edit in post-tool-use.sh; session-state persistence in stop.sh |
Blocks rm -rf /, force-pushes to main, fork bombs; runs pytest after every .py edit |
The BLOCKED_PATTERNS array in pre-tool-use.sh is regex-matched against the
literal command string passed to the Bash tool. By design, it cannot guarantee
enforcement against:
- Obfuscated commands. Encoded payloads (
echo cm0gLXJmIC8K | base64 -d | sh), indirection through environment variables (X="rm -rf /"; $X), commands sourced from disk (sh ./hidden.sh), or anything constructed at runtime can slip past a regex match. - Dangerous behaviour via non-Bash tools. The block-list runs only on the
Bashtool. AnEditthat overwrites a load-bearing config, aWritethat deletes a file by replacing its contents with empty bytes, or aWebFetchthat exfiltrates secrets is not seen by the block-list at all. - Logic-level harm. A correctly-formed
git pushto a wrong remote, a syntactically valid SQL migration that drops the wrong column, or a benign command run in a dangerous context — none of these match a syntactic pattern.
- Respect Claude Code's permission prompts. They are the primary boundary. Approving a tool call is an authorization decision; do not rely on the hook to second-guess it.
- Prefer
planmode for risky work. Plan mode prevents non-readonly tools from running until you exit plan mode explicitly. The hook block-list does not have an equivalent. - Treat the block-list as a backstop, not as authorization. The fact that a command was not blocked does not mean it is safe; it only means it did not match a pattern.
- Extend the block-list as you learn. If a real incident exposes a new class
of catastrophic command, add it to
BLOCKED_PATTERNSso the next session is protected — but always alongside the higher-layer fix (revoking a permission, switching modes, fixing the workflow).
Any shell script can be a hook. Tips:
- Always use
set -euo pipefail— fail fast and loud - Exit 0 to allow, non-zero to block (PreToolUse only)
- Write to stdout — Claude Code shows hook output to the user
- Keep them fast — hooks run on every matching event
- Idempotent — the same hook may run multiple times
Example custom hook:
#!/usr/bin/env bash
# Post-tool-use: notify on Slack when a file is edited
set -euo pipefail
FILE="${CLAUDE_TOOL_OUTPUT_FILE_PATH:-}"
[ -z "$FILE" ] && exit 0
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-d "{\"text\": \"Claude edited: $FILE\"}" > /dev/null
exit 0