Hooks From Zero to Production
Six shell scripts that run automatically to protect your environment, your git history, and your sanity. Set once. Forget it.
Why hooks matter
Claude Code can run shell commands, edit files, and interact with git on your behalf. That power is useful until it touches something it should not. Hooks are your guardrails — shell scripts that fire automatically before or after Claude acts, blocking dangerous operations and logging everything silently in the background.
Hook lifecycle
Every event Claude Code emits — and when your script runs
| Event | When it fires | Can block? | Env var |
|---|---|---|---|
| PreToolUse | Before Claude calls any tool | yes (exit 2) | CLAUDE_TOOL_INPUT |
| PostToolUse | After a tool completes | no | CLAUDE_TOOL_OUTPUT |
| Stop | When Claude finishes a task | no | — |
| Notification | On user-facing notifications | no | CLAUDE_NOTIFICATION |
The 6 hooks
Click any hook to expand the script and install instructions
Intercepts every Bash command Claude tries to run and blocks anything destructive. Prevents git commit (direct), force push, reset --hard, branch -D, and rm -rf. This is your first line of defense against Claude taking irreversible actions on your codebase without explicit approval.
- git commit (direct commits bypass your review)
- git push --force (overwrites remote history)
- git reset --hard (destroys uncommitted work)
- git branch -D (deletes branches permanently)
- rm -rf (recursive force delete)
#!/bin/bash
# bash-safety.sh — PreToolUse / Bash
# Blocks destructive git and shell commands
COMMAND="$CLAUDE_TOOL_INPUT"
# Define blocked patterns
BLOCKED_PATTERNS=(
"git commit"
"git push --force"
"git push -f"
"git reset --hard"
"git branch -D"
"git branch -d -f"
"rm -rf"
)
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qi "$pattern"; then
echo "BLOCKED: '$pattern' is not allowed."
echo "Use GitButler for commits or ask the user first."
exit 2
fi
done
exit 0Runs before any Edit or Write operation. Outright blocks access to .env files and credential files. Issues a warning (but allows) edits to CLAUDE.md, MEMORY.md, and settings.json so you are always aware when Claude modifies its own configuration.
- .env, .env.local, .env.production (secrets)
- credentials.json, credentials.* (API keys)
- CLAUDE.md (your profile — changes affect all sessions)
- MEMORY.md (session memory — data loss risk)
- settings.json (hook config — could disable safety)
#!/bin/bash
# file-protection.sh — PreToolUse / Edit + Write
# Blocks secrets, warns on sensitive config
FILE_PATH="$CLAUDE_FILE_PATH"
FILENAME=$(basename "$FILE_PATH")
# Hard block: secrets and credentials
case "$FILENAME" in
.env|.env.*|credentials.*)
echo "BLOCKED: Cannot edit $FILENAME"
echo "This file may contain secrets."
exit 2
;;
esac
# Soft warn: sensitive config
WARN_FILES="CLAUDE.md MEMORY.md settings.json"
if echo "$WARN_FILES" | grep -qw "$FILENAME"; then
echo "WARNING: Editing $FILENAME — this affects Claude config."
# Allow but warn (exit 0)
fi
exit 0Creates a timestamped copy of any file before Claude modifies it. Backups land in ~/.claude/backups/ with the original path encoded in the filename. Automatically prunes to keep only the last 10 versions per file. This gives you instant undo without needing git — useful when you are iterating fast and have not committed yet.
#!/bin/bash
# file-backup.sh — PreToolUse / Edit
# Backs up file before Claude edits it
FILE_PATH="$CLAUDE_FILE_PATH"
BACKUP_DIR="$HOME/.claude/backups"
mkdir -p "$BACKUP_DIR"
if [ ! -f "$FILE_PATH" ]; then
exit 0 # New file, nothing to back up
fi
# Encode path: /foo/bar.md -> _foo_bar.md
SAFE_NAME=$(echo "$FILE_PATH" | sed 's|/|_|g')
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
cp "$FILE_PATH" "$BACKUP_DIR/${TIMESTAMP}${SAFE_NAME}"
# Keep only last 10 backups per file
ls -t "$BACKUP_DIR"/*"$SAFE_NAME" 2>/dev/null \
| tail -n +11 \
| xargs rm -f 2>/dev/null
exit 0Intercepts Claude's web search queries and checks if any year (2020–2029) is already present. If not, it appends the current year to the query automatically. This prevents Claude from surfacing outdated documentation, deprecated APIs, or stale advice — a common problem when Claude searches without temporal context.
#!/bin/bash # search-year.sh — PreToolUse / WebSearch # Auto-appends current year to searches QUERY="$CLAUDE_TOOL_INPUT" CURRENT_YEAR=$(date +%Y) # Check if a year is already in the query if echo "$QUERY" | grep -qE "20[2-3][0-9]"; then exit 0 # Year present, no change needed fi # Append current year to the query echo "MODIFIED_QUERY: $QUERY $CURRENT_YEAR" exit 0
Fires after every Write or Edit operation and appends a line to a daily log file at ~/.claude/changes-YYYY-MM-DD.log. Each entry records the timestamp, tool used, and file path. Gives you a complete audit trail of everything Claude touched during a session — useful for debugging, reviewing, or just knowing what happened.
#!/bin/bash # file-logger.sh — PostToolUse / Write + Edit # Logs every file change to daily log FILE_PATH="$CLAUDE_FILE_PATH" TOOL_NAME="$CLAUDE_TOOL_NAME" LOG_DIR="$HOME/.claude" LOG_FILE="$LOG_DIR/changes-$(date +%Y-%m-%d).log" TIMESTAMP=$(date +"%H:%M:%S") echo "[$TIMESTAMP] $TOOL_NAME → $FILE_PATH" >> "$LOG_FILE" exit 0
Fires when Claude completes a task (Stop event). Plays the macOS Glass system sound and sends an osascript notification so you see it even if the terminal is in the background. Perfect for long-running tasks — kick off a build, switch tabs, and get pulled back when it is done.
#!/bin/bash # stop-notify.sh — Stop event # Notification when Claude finishes # Play system sound afplay /System/Library/Sounds/Glass.aiff & # Send macOS notification osascript -e 'display notification "Task complete." with title "Claude Code" sound name "Glass"' exit 0
Activate all hooks in settings.json
Wire up all six with a single config block
Add this configuration to your ~/.claude/settings.json to wire up all six hooks. Each entry maps a hook lifecycle event to the script that handles it. Once saved, hooks activate immediately on the next Claude Code session.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/bash-safety.sh" }
]
},
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/file-protection.sh" }
]
},
{
"matcher": "Edit",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/file-backup.sh" }
]
},
{
"matcher": "WebSearch",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/search-year.sh" }
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/file-logger.sh" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/stop-notify.sh" }
]
}
]
}
}Debugging hooks
Three ways to verify your hooks are wired correctly