Skip to main content
blog.philz.dev

Git Commit Prompt Hook

Here's a git hook that rejects commit messages if they’re coming from a coding agent and don’t have a "Prompt: " section. It works well.

#!/usr/bin/env python3
#
# Git commit-msg hook that requires a "Prompt:" section in agent-driven commits.
#
# Agent commits are detected by:
#   1. Commit message markers (Generated with [Claude Code], Co-Authored-By, etc.)
#   2. Parent process names (claude, cursor, aider, copilot, gemini, etc.)

import os
import re
import subprocess
import sys

AGENT_PROCESSES = [
    "claude", "cursor", "aider", "copilot", "cody",
    "codium", "windsurf", "gemini", "cline", "continue", "shelley"
]

AGENT_MARKERS_PATTERN = re.compile(
    r"Generated with \[Claude|Co-Authored-By: Claude|🤖 Generated|"
    r"Generated by (Claude|Cursor|Copilot|Aider|Gemini)",
    re.IGNORECASE
)

def get_process_name(pid: int) -> str | None:
    """Get the process name for a given PID."""
    try:
        result = subprocess.run(
            ["ps", "-p", str(pid), "-o", "comm="],
            capture_output=True,
            text=True,
            check=False
        )
        return result.stdout.strip() if result.returncode == 0 else None
    except Exception:
        return None


def get_parent_pid(pid: int) -> int | None:
    """Get the parent PID for a given PID."""
    try:
        result = subprocess.run(
            ["ps", "-p", str(pid), "-o", "ppid="],
            capture_output=True,
            text=True,
            check=False
        )
        if result.returncode == 0:
            ppid = result.stdout.strip()
            return int(ppid) if ppid else None
        return None
    except Exception:
        return None


def check_commit_message_markers(commit_msg: str) -> bool:
    """Check if commit message contains agent markers."""
    return bool(AGENT_MARKERS_PATTERN.search(commit_msg))


def check_parent_processes() -> tuple[bool, str | None]:
    """Check parent process tree for known agent names."""
    pid = os.getpid()
    while pid and pid != 1:
        proc_name = get_process_name(pid)
        if proc_name:
            proc_name_lower = proc_name.lower()
            for agent in AGENT_PROCESSES:
                if proc_name_lower == agent:
                    return True, proc_name
        pid = get_parent_pid(pid)
    return False, None


def main() -> int:
    if len(sys.argv) < 2:
        return 0

    commit_msg_file = sys.argv[1]
    try:
        with open(commit_msg_file, "r") as f:
            commit_msg = f.read()
    except Exception:
        return 0

    # Check if this is an agent-driven commit
    is_agent_commit = False
    detected_by = ""

    # Method 1: Check commit message for agent markers
    if check_commit_message_markers(commit_msg):
        is_agent_commit = True
        detected_by = "commit message markers"

    # Method 2: Check parent processes for known agent names
    if not is_agent_commit:
        found, proc_name = check_parent_processes()
        if found:
            is_agent_commit = True
            detected_by = f"parent process '{proc_name}'"

    # If it's an agent commit, require a "Prompt:" section
    if is_agent_commit:
        if not re.search(r"^Prompt:", commit_msg, re.MULTILINE):
            print("ERROR: Agent-driven commits must include a 'Prompt:' section.")
            print()
            print("This commit appears to be driven by a coding agent")
            print(f"(detected via {detected_by}).")
            print()
            print("Please add a line starting with 'Prompt:' with the exact prompt")
            print("the user used that led to this change, including any follow up commands.")
            print("Put it on the top of the commit message body.")
            return 1

    return 0


if __name__ == "__main__":
    sys.exit(main())

Yes, it's vibe-coded.

Yes, I hate commit hooks too, but this one has been foisted on me by myself and runs instantaneously.