diff --git a/.config/git-hooks/README.md b/.config/git-hooks/README.md new file mode 100644 index 0000000..db2abe9 --- /dev/null +++ b/.config/git-hooks/README.md @@ -0,0 +1,114 @@ +# AI-Blocking Git Hooks + +Git hooks that prevent AI indicators from being committed to your repository. + +## What These Hooks Block + +The hooks scan for AI references including: + +- AI tool names (Claude, Cursor, Copilot, ChatGPT, Gemini, etc.) +- AI-related phrases ("ai-generated", "ai-assisted", "generated with", etc.) +- Model names (GPT-4, Claude Sonnet, Claude Code, etc.) +- Company names (OpenAI, Anthropic, etc.) +- Co-authorship markers (Co-Authored-By: Claude, noreply@anthropic.com, etc.) + +## Prerequisites + +Install [pre-commit](https://pre-commit.com/): + +```bash +pip install pre-commit +``` + +## Installation + +### Single Repository + +1. Navigate to your repository: + + ```bash + cd /path/to/your/repo + ``` + +2. Create or update `.pre-commit-config.yaml`: + + ```yaml + repos: + - repo: local + hooks: + - id: ai-pre-commit + name: Block AI indicators in staged files + entry: python3 .config/ai-hooks/ai_guard.py pre-commit + language: system + pass_filenames: false + stages: [pre-commit] + - id: ai-commit-msg + name: Block AI indicators in commit messages + entry: python3 .config/ai-hooks/ai_guard.py commit-msg + language: system + stages: [commit-msg] + ``` + +3. Install the hooks: + + ```bash + pre-commit install + ``` + +## How It Works + +### commit-msg Hook + +- Runs when you create a commit +- Scans the commit message for AI indicators +- Blocks the commit if any indicators are found + +### pre-commit Hook + +- Runs before the commit is created +- Scans all staged files for AI indicators +- Blocks the commit if any indicators are found in the code + +## Uninstallation + +To remove the hooks from a repository: + +```bash +cd /path/to/your/repo +rm .git/hooks/commit-msg .git/hooks/pre-commit .git/hooks/ai_guard.py +``` + +## Testing + +To verify the hooks are working: + +1. Create a test file with AI indicators: + + ```bash + echo "Generated with Claude Code" > test.txt + git add test.txt + git commit -m "test" + ``` + +2. You should see an error message blocking the commit + +3. Clean up: + ```bash + git reset HEAD test.txt + rm test.txt + ``` + +## Bypassing Hooks When Needed + +In rare cases where you need to commit content with AI indicators, you can bypass the hooks using the `--no-verify` flag: + +```bash +git commit --no-verify -m "your commit message" +``` + +## Notes + +- These hooks are **repository-specific** and must be installed in each repo where you want them active +- Git hooks are not tracked by git itself (they live in `.git/hooks/`) +- If you have existing hooks, the installer will create `.backup` copies +- All pattern matching is case-insensitive diff --git a/.config/git-hooks/ai_guard.py b/.config/git-hooks/ai_guard.py new file mode 100755 index 0000000..d4e8df2 --- /dev/null +++ b/.config/git-hooks/ai_guard.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +""" +Python wrapper for AI indicator guard hooks. +Calls the appropriate bash script based on the hook type. +""" + +import subprocess +import sys +from pathlib import Path + + +def main(): + if len(sys.argv) < 2: + print("Usage: ai_guard.py ", file=sys.stderr) + sys.exit(1) + + hook_type = sys.argv[1] + + # Get the directory where this script is located + script_dir = Path(__file__).parent + + # Determine which hook script to call + if hook_type == "pre-commit": + hook_script = script_dir / "pre-commit" + elif hook_type == "commit-msg": + hook_script = script_dir / "commit-msg" + # For commit-msg, we need to pass the commit message file + if len(sys.argv) < 3: + print("Error: commit-msg hook requires message file argument", file=sys.stderr) + sys.exit(1) + commit_msg_file = sys.argv[2] + else: + print(f"Unknown hook type: {hook_type}", file=sys.stderr) + sys.exit(1) + + if not hook_script.exists(): + print(f"Hook script not found: {hook_script}", file=sys.stderr) + sys.exit(1) + + # Call the hook script + try: + if hook_type == "commit-msg": + result = subprocess.run( + [str(hook_script), commit_msg_file], + check=False + ) + else: + result = subprocess.run( + [str(hook_script)], + check=False + ) + sys.exit(result.returncode) + except Exception as e: + print(f"Error running hook: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.config/git-hooks/commit-msg b/.config/git-hooks/commit-msg new file mode 100755 index 0000000..9c4e129 --- /dev/null +++ b/.config/git-hooks/commit-msg @@ -0,0 +1,118 @@ +#!/usr/bin/env bash + +# Guard commit messages against explicit AI usage indicators. +# The script rejects messages containing phrases that suggest the text +# was generated by an AI assistant or tool (as defined in onboarding docs). + +set -euo pipefail + +MSG_FILE=${1:?} + +# Patterns to block; all matches are case-insensitive (grep -i). +declare -a PATTERNS=( + "generated by cursor" + "cursor ai" + "cursor wrote this" + "cursor suggestion" + "generated by claude" + "claude wrote this" + "claude ai" + "claude opus" + "claude sonnet" + "claude haiku" + "claude code" + "generated with claude code" + "generated with \\[claude code\\]" + "co-authored-by: claude" + "noreply@anthropic\\.com" + "generated by gemini" + "gemini wrote this" + "google gemini" + "gemini ultra" + "gemini pro" + "gemini nano" + "generated by bard" + "bard wrote this" + "google bard" + "as an ai assistant" + "as an ai" + "ai assistant" + "ai suggestion" + "ai suggestions" + "ai-generated" + "ai generated" + "ai-assisted" + "ai assisted" + "ai-powered" + "ai powered" + "powered by ai" + "with ai assistance" + "with the help of ai" + "assisted by ai" + "written with ai" + "written by ai" + "generated with ai" + "generated via ai" + "created with ai" + "created by ai" + "authored by ai" + "co-authored by ai" + "ai help" + "ai helped" + "ai copilot" + "ai co-pilot" + "chatgpt" + "gpt-4" + "gpt4" + "gpt-3" + "gpt3" + "openai" + "anthropic" + "google ai" + "google deepmind" + "deepmind" + "perplexity" + "perplexity ai" + "tabnine" + "tabnine ai" + "replit ghostwriter" + "ghostwriter ai" + "aws codewhisperer" + "amazon codewhisperer" + "code whisperer" + "codewhisperer" + "github copilot" + "copilot" + "amazon q" + "amazon q developer" + "meta ai" + "deepseek" + "phind" + "blackbox ai" + "magic ai" + "ai model" + "llm" + "large language model" + "language model" + "\\[ai\\]" + "\\[ai-generated\\]" + "\\[cursor\\]" + "\\[claude\\]" + "\\[gemini\\]" + "\\[copilot\\]" + "\\[chatgpt\\]" + "\\[llm\\]" +) + +for pattern in "${PATTERNS[@]}"; do + if LC_ALL=C grep -qiE "$pattern" "$MSG_FILE"; then + cat >&2 < "* ]]; then + path=${path##* -> } + fi + if [[ "$added" == "-" || "$deleted" == "-" ]]; then + BINARY_PATHS="${BINARY_PATHS}${path}"$'\n' + fi +done < <(git diff --cached --numstat --diff-filter=ACMRT || true) + +report="" +current_file="" +last_report_file="" +violations=0 + +while IFS= read -r line; do + case "$line" in + "diff --git "*) + current_file="" + ;; + "+++ "*) + current_file=${line#+++ } + if [[ "$current_file" == b/* ]]; then + current_file=${current_file#b/} + elif [[ "$current_file" == a/* ]]; then + current_file=${current_file#a/} + fi + if [[ "$current_file" == "/dev/null" ]]; then + current_file="" + fi + ;; + "Binary files "*) + current_file="" + ;; + *) + if [[ "$line" == +* && "$line" != "+++"* ]]; then + [[ -z "$current_file" ]] && continue + if [[ -n "$BINARY_PATHS" ]] && echo "$BINARY_PATHS" | grep -qxF "$current_file"; then + continue + fi + content=${line:1} + if [[ -n "$content" ]] && LC_ALL=C grep -qiE "$PATTERN_REGEX" <<<"$content"; then + if [[ "$current_file" != "$last_report_file" ]]; then + report+=$'\n'"• $current_file" + last_report_file="$current_file" + fi + report+=$'\n'" ${line}" + violations=1 + fi + fi + ;; + esac +done < <(git diff --cached --diff-filter=ACMRT --unified=0 --no-color) + +if [[ $violations -eq 1 ]]; then + cat >&2 <