Documentation
Hook
Hooks
What claude-code-mastery runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
$ npx -y skills add TheDecipherist/claude-code-mastery --agent claude-codeShips with claude-code-mastery. Installing the plugin gets these hooks.
Where it lives
- hooks/after-edit.shGitHub
Read the script
#!/usr/bin/env bash # ============================================================================= # PostToolUse Hook: After File Edit # ============================================================================= # # This hook runs AFTER Claude edits or writes a file. # Use it for fast operations like formatting that should run immediately. # # For heavier checks (tests, full linting), use the end-of-turn (Stop) hook. # # Usage: # Add to ~/.claude/settings.json: # { # "hooks": { # "PostToolUse": [ # { # "matcher": "Edit|Write", # "hooks": [ # { # "type": "command", # "command": "~/.claude/hooks/after-edit.sh" # } # ] # } # ] # } # } # ============================================================================= set -euo pipefail # Read JSON input from stdin INPUT=$(cat) # Extract the file path FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty') if [[ -z "$FILE_PATH" ]]; then exit 0 # No file path, nothing to do fi # Get file extension EXTENSION="${FILE_PATH##*.}" # ----------------------------------------------------------------------------- # Format based on file type # ----------------------------------------------------------------------------- case "$EXTENSION" in js|jsx|ts|tsx|json|md|yaml|yml|css|scss|html) # Prettier for web files if command -v prettier &>/dev/null; then prettier --write "$FILE_PATH" 2>/dev/null || true fi ;; py) # Black for Python if command -v black &>/dev/null; then black --quiet "$FILE_PATH" 2>/dev/null || true fi # Ruff for linting if command -v ruff &>/dev/null; then ruff check --fix --silent "$FILE_PATH" 2>/dev/null || true fi ;; go) # gofmt for Go if command -v gofmt &>/dev/null; then gofmt -w "$FILE_PATH" 2>/dev/null || true fi ;; rs) # rustfmt for Rust if command -v rustfmt &>/dev/null; then rustfmt "$FILE_PATH" 2>/dev/null || true fi ;; sh|bash) # shfmt for shell scripts if command -v shfmt &>/dev/null; then shfmt -w "$FILE_PATH" 2>/dev/null || true fi ;; esac # Always exit 0 - formatting failures shouldn't block work exit 0 - hooks/block-dangerous-commands.shGitHub
Read the script
#!/usr/bin/env bash # ============================================================================= # PreToolUse Hook: Block Dangerous Bash Commands # ============================================================================= # # This hook runs BEFORE bash commands execute. # It blocks destructive patterns like rm -rf, force pushes, etc. # # Exit codes: # 0 = Allow command # 2 = Block command (stderr fed back to Claude) # # Usage: # Add to ~/.claude/settings.json: # { # "hooks": { # "PreToolUse": [ # { # "matcher": "Bash", # "hooks": [ # { # "type": "command", # "command": "~/.claude/hooks/block-dangerous-commands.sh" # } # ] # } # ] # } # } # ============================================================================= set -euo pipefail # Read JSON input from stdin INPUT=$(cat) # Extract the command using jq COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') if [[ -z "$COMMAND" ]]; then exit 0 # No command, allow fi # ----------------------------------------------------------------------------- # Dangerous Patterns # ----------------------------------------------------------------------------- # rm -rf with dangerous paths if echo "$COMMAND" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|--recursive\s+--force|-rf|-fr)\s+(/|~|\.\.|\$HOME|\$\{HOME\})'; then echo "๐ BLOCKED: Destructive rm command targeting root, home, or parent directory" >&2 echo "Command: $COMMAND" >&2 exit 2 fi # rm -rf /* or rm -rf ~/* if echo "$COMMAND" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|--recursive\s+--force|-rf|-fr)\s+(/\*|~/\*|/home)'; then echo "๐ BLOCKED: Destructive rm command with wildcard on sensitive path" >&2 echo "Command: $COMMAND" >&2 exit 2 fi # Force push to main/master if echo "$COMMAND" | grep -qE 'git\s+push\s+.*(-f|--force)\s+.*(main|master|production|release)'; then echo "๐ BLOCKED: Force push to protected branch" >&2 echo "Command: $COMMAND" >&2 echo "Tip: Create a PR instead of force pushing to main/master" >&2 exit 2 fi # chmod 777 (world-writable) if echo "$COMMAND" | grep -qE 'chmod\s+(777|a\+rwx)'; then echo "โ ๏ธ BLOCKED: Setting world-writable permissions (777)" >&2 echo "Command: $COMMAND" >&2 echo "Tip: Use 755 for directories, 644 for files" >&2 exit 2 fi # Piping curl directly to shell (dangerous pattern) if echo "$COMMAND" | grep -qE 'curl\s+.*\|\s*(ba)?sh'; then echo "โ ๏ธ BLOCKED: Piping curl output directly to shell" >&2 echo "Command: $COMMAND" >&2 echo "Tip: Download script first, review it, then execute" >&2 exit 2 fi # wget piped to shell if echo "$COMMAND" | grep -qE 'wget\s+.*\|\s*(ba)?sh'; then echo "โ ๏ธ BLOCKED: Piping wget output directly to shell" >&2 echo "Command: $COMMAND" >&2 exit 2 fi # dd writing to disk devices if echo "$COMMAND" | grep -qE 'dd\s+.*of=/dev/(sd|hd|nvme|disk)'; then echo "๐ BLOCKED: dd command writing directly to disk device" >&2 echo "Command: $COMMAND" >&2 exit 2 fi # mkfs (format disk) if echo "$COMMAND" | grep -qE 'mkfs'; then echo "๐ BLOCKED: mkfs command (disk formatting)" >&2 echo "Command: $COMMAND" >&2 exit 2 fi # Commands that could exfiltrate data if echo "$COMMAND" | grep -qE '(curl|wget|nc|netcat)\s+.*\.(env|pem|key|secret)'; then echo "โ ๏ธ BLOCKED: Command appears to exfiltrate sensitive files" >&2 echo "Command: $COMMAND" >&2 exit 2 fi # Reading .env files via cat/less/head/tail if echo "$COMMAND" | grep -qE '(cat|less|head|tail|more|bat)\s+.*\.env'; then echo "โ ๏ธ BLOCKED: Reading .env file via $COMMAND" >&2 echo "Tip: Use environment variables instead of reading .env directly" >&2 exit 2 fi # ----------------------------------------------------------------------------- # Command is safe, allow it # ----------------------------------------------------------------------------- exit 0 - hooks/block-secrets.pyGitHub
Read the script
#!/usr/bin/env python3 """ PreToolUse hook to block access to sensitive files. This hook runs BEFORE Claude can read, edit, or write files. Exit code 2 blocks the operation and feeds stderr back to Claude. Usage: Add to ~/.claude/settings.json: { "hooks": { "PreToolUse": [ { "matcher": "Read|Edit|Write", "hooks": [ { "type": "command", "command": "python3 ~/.claude/hooks/block-secrets.py" } ] } ] } } Why this matters: - CLAUDE.md rules are suggestions that Claude can override - This hook is deterministic enforcement - it ALWAYS runs - Even if Claude is "convinced" to read secrets, this blocks it """ import json import sys from pathlib import Path # ============================================================================= # CONFIGURATION - Customize these patterns for your environment # ============================================================================= # Exact filenames to block SENSITIVE_FILENAMES = { # Environment files '.env', '.env.local', '.env.development', '.env.development.local', '.env.test', '.env.test.local', '.env.production', '.env.production.local', '.env.staging', # Secrets files 'secrets.json', 'secrets.yaml', 'secrets.yml', 'secrets.toml', '.secrets', # Credentials 'credentials.json', 'credentials.yaml', 'service-account.json', 'service_account.json', # SSH keys 'id_rsa', 'id_rsa.pub', 'id_ed25519', 'id_ed25519.pub', 'id_ecdsa', 'id_dsa', 'known_hosts', 'authorized_keys', # Package manager auth '.npmrc', '.pypirc', '.yarnrc', '.docker/config.json', # Cloud credentials '.aws/credentials', '.aws/config', 'gcloud/credentials.db', '.azure/credentials', # Git credentials '.git-credentials', '.gitconfig', # Can contain credentials '.git/config', # Can contain tokens # Database '.pgpass', '.my.cnf', '.mongorc.js', } # File extensions to block SENSITIVE_EXTENSIONS = { '.pem', # Certificates/keys '.key', # Private keys '.p12', # PKCS#12 certificates '.pfx', # Windows certificates '.jks', # Java keystore '.keystore', # Generic keystore '.crt', # Certificates (sometimes contain keys) '.cer', # Certificates } # Patterns to match anywhere in path SENSITIVE_PATH_PATTERNS = [ 'secret', 'credential', 'private_key', 'privatekey', '.env.', # Catches .env.anything '/secrets/', # Secrets directories ] # ============================================================================= # HOOK LOGIC # ============================================================================= def is_sensitive_file(file_path: str) -> tuple[bool, str]: """ Check if a file path matches sensitive patterns. Returns (is_sensitive, reason). """ if not file_path: return False, "" path = Path(file_path) file_name = path.name file_lower = file_path.lower() # Check exact filename match if file_name in SENSITIVE_FILENAMES: return True, f"'{file_name}' is a known sensitive file" # Check extension if path.suffix.lower() in SENSITIVE_EXTENSIONS: return True, f"'{path.suffix}' files may contain private keys or certificates" # Check path patterns for pattern in SENSITIVE_PATH_PATTERNS: if pattern in file_lower: return True, f"path contains sensitive pattern '{pattern}'" return False, "" def extract_file_path(data: dict) -> str: """ Extract file path from tool input. Different tools use different parameter names. """ tool_input = data.get('tool_input', {}) # Try common parameter names for key in ['file_path', 'path', 'filename', 'file']: if key in tool_input: return tool_input[key] # For Bash tool, check the command for file references command = tool_input.get('command', '') if command: # This is a simplified check - you might want more sophisticated parsing for pattern in SENSITIVE_FILENAMES: if pattern in command: return pattern for ext in SENSITIVE_EXTENSIONS: if ext in command: return command # Return command as "file" so it gets blocked return "" def main(): try: # Read JSON input from stdin data = json.load(sys.stdin) # Get tool name for better error messages tool_name = data.get('tool_name', 'unknown') # Extract file path file_path = extract_file_path(data) if not file_path: # No file path found, allow the operation sys.exit(0) # Check if sensitive is_sensitive, reason = is_sensitive_file(file_path) if is_sensitive: # Construct error message that will be fed back to Claude error_msg = f""" โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ ๐ SECURITY HOOK BLOCKED โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ โ Tool: {tool_name} โ File: {file_path} โ โ Reason: {reason} โ โ This file likely contains secrets, credentials, or private keys โ that should not be accessed programmatically. โ โ Recommended actions: โ โข Use environment variables instead of reading .env directly โ โข Ask the user for specific (non-sensitive) information โ โข Reference .env.example for variable names only โ โข Store secrets in a proper secrets manager โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ """.strip() # Print to stderr (will be fed back to Claude) print(error_msg, file=sys.s - hooks/end-of-turn.shGitHub
Read the script
#!/usr/bin/env bash # ============================================================================= # End-of-Turn Quality Gate Hook # ============================================================================= # # This hook runs when Claude finishes responding (Stop event). # It performs quality checks to catch issues before they accumulate. # # Usage: # Add to ~/.claude/settings.json or .claude/settings.json: # { # "hooks": { # "Stop": [ # { # "matcher": "*", # "hooks": [ # { # "type": "command", # "command": "~/.claude/hooks/end-of-turn.sh" # } # ] # } # ] # } # } # # Exit codes: # 0 = Success (continue normally) # 1 = Error shown to user # 2 = Block and feed stderr to Claude (use sparingly for Stop hooks) # # ============================================================================= set -euo pipefail # ----------------------------------------------------------------------------- # Configuration # ----------------------------------------------------------------------------- # Set to "true" to enable verbose logging VERBOSE="${CLAUDE_HOOK_VERBOSE:-false}" # Maximum time for checks (seconds) TIMEOUT=30 # ----------------------------------------------------------------------------- # Helper Functions # ----------------------------------------------------------------------------- log() { if [[ "$VERBOSE" == "true" ]]; then echo "[end-of-turn] $*" >&2 fi } run_check() { local name="$1" local cmd="$2" log "Running: $name" if timeout "$TIMEOUT" bash -c "$cmd" 2>/dev/null; then log "โ $name passed" return 0 else log "โ $name failed (non-blocking)" return 0 # Don't fail the hook, just log fi } # ----------------------------------------------------------------------------- # Detect Project Type # ----------------------------------------------------------------------------- is_nodejs() { [[ -f "package.json" ]] } is_typescript() { [[ -f "tsconfig.json" ]] } is_python() { [[ -f "pyproject.toml" ]] || [[ -f "setup.py" ]] || [[ -f "requirements.txt" ]] } is_rust() { [[ -f "Cargo.toml" ]] } is_go() { [[ -f "go.mod" ]] } # ----------------------------------------------------------------------------- # Project-Specific Checks # ----------------------------------------------------------------------------- check_nodejs() { log "Detected Node.js project" # Check if node_modules exists if [[ ! -d "node_modules" ]]; then log "node_modules missing, skipping npm checks" return 0 fi # Run lint if available if grep -q '"lint"' package.json 2>/dev/null; then run_check "npm lint" "npm run lint --silent" fi # Run typecheck if TypeScript if is_typescript; then if grep -q '"typecheck"' package.json 2>/dev/null; then run_check "typecheck" "npm run typecheck --silent" elif command -v tsc &>/dev/null; then run_check "tsc" "tsc --noEmit" fi fi } check_python() { log "Detected Python project" # Ruff (fast Python linter) if command -v ruff &>/dev/null; then run_check "ruff" "ruff check . --fix --silent" fi # Black (formatter) if command -v black &>/dev/null; then run_check "black" "black --check --quiet ." fi # MyPy (type checker) if command -v mypy &>/dev/null && [[ -f "mypy.ini" || -f "pyproject.toml" ]]; then run_check "mypy" "mypy . --silent-imports" fi } check_rust() { log "Detected Rust project" # Cargo check (fast type checking) if command -v cargo &>/dev/null; then run_check "cargo check" "cargo check --quiet" fi # Clippy (linter) if command -v cargo &>/dev/null; then run_check "clippy" "cargo clippy --quiet -- -D warnings" fi } check_go() { log "Detected Go project" # Go vet if command -v go &>/dev/null; then run_check "go vet" "go vet ./..." fi # Staticcheck if command -v staticcheck &>/dev/null; then run_check "staticcheck" "staticcheck ./..." fi } # ----------------------------------------------------------------------------- # Universal Checks # ----------------------------------------------------------------------------- check_secrets() { log "Checking for exposed secrets" # Simple grep for common secret patterns in staged files if git rev-parse --git-dir &>/dev/null; then local staged_files staged_files=$(git diff --cached --name-only 2>/dev/null || true) if [[ -n "$staged_files" ]]; then # Check for hardcoded secrets (simplified pattern) if echo "$staged_files" | xargs grep -l -E "(API_KEY|SECRET|TOKEN|PASSWORD)\s*[=:]\s*['\"][A-Za-z0-9_\-]{16,}" 2>/dev/null; then echo "โ ๏ธ Warning: Possible hardcoded secrets in staged files" >&2 fi fi fi } check_env_committed() { log "Checking .env not staged" if git rev-parse --git-dir &>/dev/null; then if git diff --cached --name-only 2>/dev/null | grep -q "^\.env"; then echo "โ ๏ธ Warning: .env file is staged for commit!" >&2 fi fi } # ----------------------------------------------------------------------------- # Main # ----------------------------------------------------------------------------- main() { log "Starting end-of-turn checks" # Run project-specific checks if is_nodejs; then check_nodejs fi if is_python; then check_python fi if is_rust; then check_rust fi if is_go; then check_go fi # Universal checks check_secrets check_env_committed log "End-of-turn checks complete" exit 0 } main "$@" - hooks/notify.shGitHub
Read the script
#!/usr/bin/env bash # ============================================================================= # Notification Hook: Desktop Alerts # ============================================================================= # # This hook runs when Claude Code sends notifications. # It triggers desktop notifications so you know when Claude needs input. # # Works on: # - macOS (osascript) # - Linux (notify-send) # - Windows WSL (powershell) # # Usage: # Add to ~/.claude/settings.json: # { # "hooks": { # "Notification": [ # { # "matcher": "*", # "hooks": [ # { # "type": "command", # "command": "~/.claude/hooks/notify.sh" # } # ] # } # ] # } # } # ============================================================================= set -euo pipefail # Read JSON input from stdin INPUT=$(cat) # Extract notification content CONTENT=$(echo "$INPUT" | jq -r '.content // "Claude needs your attention"') # Truncate long messages if [[ ${#CONTENT} -gt 100 ]]; then CONTENT="${CONTENT:0:100}..." fi # ----------------------------------------------------------------------------- # Send notification based on OS # ----------------------------------------------------------------------------- send_notification() { local title="Claude Code" local message="$1" # macOS if [[ "$OSTYPE" == "darwin"* ]]; then osascript -e "display notification \"$message\" with title \"$title\" sound name \"Glass\"" 2>/dev/null || true return fi # Linux with notify-send if command -v notify-send &>/dev/null; then notify-send "$title" "$message" -u normal -t 5000 2>/dev/null || true return fi # Windows WSL if grep -qi microsoft /proc/version 2>/dev/null; then powershell.exe -Command " [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null \$template = '<toast><visual><binding template=\"ToastText02\"><text id=\"1\">$title</text><text id=\"2\">$message</text></binding></visual></toast>' \$xml = New-Object Windows.Data.Xml.Dom.XmlDocument \$xml.LoadXml(\$template) \$toast = [Windows.UI.Notifications.ToastNotification]::new(\$xml) [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Claude Code').Show(\$toast) " 2>/dev/null || true return fi # Fallback: terminal bell echo -e '\a' } send_notification "$CONTENT" exit 0
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withclaude-code-mastery
The complete guide to maximizing Claude Code: Global CLAUDE.md, MCP Servers, Commands, Hooks, Skills, and Why Single-Purpose Chats Matter. This version is obsolete by now.
Get the whole plugin

