claudeforge
Automatically creates, enhances and maintains CLAUDE.md files for Claude Code projects, so you don't have to write and update them by hand.
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.
Claude Code
$ npx -y skills add TheDecipherist/claude-code-mastery --agent claude-code
Cursor
Cursor also runs Claude Code hooks from .claude/settings.json, so copy these hooks there to use them in Cursor.
Where it lives
#!/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
#!/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
#!/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#!/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 "$@"
#!/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.
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.
Automatically creates, enhances and maintains CLAUDE.md files for Claude Code projects, so you don't have to write and update them by hand.
Gives your coding agent skills to author, maintain, validate and visualize Open Knowledge Format bundles, portable markdown knowledge that both your team and your agents can read. Includes a backfill skill for building a bundle from an existing repository's git history and session transcripts.
Turns design docs and plan drafts into HTML pages you can comment on inline. Fires when you ask to review a design, preview a plan, or render visual output.
A Claude Code plugin that maintains FILETREE.md, a one-line description per file with content hashes for staleness detection. Lets the LLM grasp repo layout in a few hundred tokens before touching code.
Keeps you fluent in code that AI writes: each feature is designed, built with the reasoning behind each decision explained, and recorded as docs, decisions and review summaries, so the understanding stays with the code.
A Claude Code plugin that reads your codebase and draws its infrastructure and architecture with D2, then keeps those diagrams in step with the code as it changes. Command: /d2:diagram.
Shows which docs your AI agent actually discovers, using local telemetry with no token cost. Includes a CI gate that fails a pull request with the affected file and a suggested fix.