Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ Overview
**Commit Check** is a lightweight policy engine for Git commit metadata.

It validates commit messages, branch names, author identity, signoff trailers,
and push safety — using one versioned TOML policy across local hooks, CI,
GitHub Actions, and AI automation.
AI attribution policy, and push safety — using one versioned TOML policy across
local hooks, CI, GitHub Actions, and AI automation.

- **One policy file:** ``cchk.toml``
- **Multiple enforcement points:** CLI, pre-commit, CI / GitHub Actions
Expand Down Expand Up @@ -139,6 +139,9 @@ To customize the behavior, create a configuration file named ``cchk.toml`` or ``
require_signed_off_by = false
# Bypass checks for bot/automation authors and co-authors:
ignore_authors = ["dependabot[bot]", "renovate[bot]", "copilot[bot]"]
# AI attribution policy: "ignore" (default) or "forbid"
# "forbid" rejects commits with known AI tool signatures
ai_attribution = "forbid"

[branch]
# https://conventionalbranch.org
Expand Down
3 changes: 3 additions & 0 deletions commit_check/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,8 @@
"require_signed_off_by": False,
}

# AI attribution defaults
DEFAULT_AI_ATTRIBUTION = "ignore" # "ignore" | "forbid"


__version__ = version("commit-check")
76 changes: 76 additions & 0 deletions commit_check/ai_signatures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""AI tool signature detection logic.

This module provides the public API for detecting AI tool signatures in commit
messages. The signature data (tool definitions and patterns) lives in
:mod:`commit_check.ai_signatures_data`.

Typical usage::

from commit_check.ai_signatures import detect_ai_signatures

result = detect_ai_signatures(
"feat: init\\n\\nCo-authored-by: Claude <noreply@anthropic.com>"
)
"""

from __future__ import annotations

import re

from commit_check.ai_signatures_data import ALL_KNOWN_TOOLS as _ALL_KNOWN_TOOLS

# Re-export for convenience — consumers can import everything from
# commit_check.ai_signatures without knowing about the data/logic split.
ALL_KNOWN_TOOLS = _ALL_KNOWN_TOOLS


#: Flat list of all compiled patterns for bulk scanning.
#: Each tuple is ``(regex, tool_name, description, kind)``.
ALL_PATTERNS: list[tuple[re.Pattern[str], str, str, str]] = [
(p.regex, tool.name, p.description, p.kind)
for tool in ALL_KNOWN_TOOLS
for p in tool.patterns
]


def detect_ai_signatures(message: str) -> list[dict[str, str]]:
"""Scan *message* for known AI tool signatures.

:param message: The full commit message (subject + body) to scan.
:returns: A list of dicts, one per matched signature, each with keys
``"tool"``, ``"kind"``, ``"description"``, and ``"matched_text"``.
Returns an empty list when no signatures are found.

Example::

>>> detect_ai_signatures(
... "feat: init\\n\\nCo-authored-by: Claude <noreply@anthropic.com>"
... )
[{'tool': 'Claude Code', 'kind': 'trailer', ...}]
"""
results: list[dict[str, str]] = []
seen: set[str] = set()

for regex, tool_name, desc, kind in ALL_PATTERNS:
for match in regex.finditer(message):
matched = match.group(0).strip()
if matched not in seen:
seen.add(matched)
results.append(
{
"tool": tool_name,
"kind": kind,
"description": desc,
"matched_text": matched,
}
)

return results


def has_ai_signature(message: str) -> bool:
"""Return ``True`` if *message* contains any known AI signature."""
for regex, _tool_name, _desc, _kind in ALL_PATTERNS:
if regex.search(message):
return True
return False
257 changes: 257 additions & 0 deletions commit_check/ai_signatures_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
"""Known AI tool signatures — pure data, no detection logic.

This module defines the data structures and the curated registry of known AI
coding tool signatures. To add a new tool, define a ``KnownAiTool`` entry
with its patterns and add it to ``ALL_KNOWN_TOOLS``.

The detection logic lives in :mod:`commit_check.ai_signatures`.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field


@dataclass(frozen=True)
class AiSignaturePattern:
"""A single pattern that identifies AI tool usage in a commit message.

:param regex: A compiled regex that, if matched anywhere in the commit
message body, indicates the corresponding tool was involved.
:param kind: ``"trailer"`` for structured ``Key: value`` footer lines
(matched case-insensitively), ``"body_marker"`` for any other text
marker.
:param description: Human-readable description of what is matched.
"""

regex: re.Pattern[str]
kind: str # "trailer" | "body_marker"
description: str = ""


@dataclass(frozen=True)
class KnownAiTool:
"""A known AI coding tool and its commit-message signatures.

:param name: Short display name (e.g. ``"Claude Code"``, ``"GitHub Copilot"``).
:param patterns: One or more signature patterns that indicate this tool.
"""

name: str
patterns: list[AiSignaturePattern] = field(default_factory=list)


# ---------------------------------------------------------------------------
# Pattern helpers
# ---------------------------------------------------------------------------


def _trailer(
key: str, value_pattern: str = r".*", description: str = ""
) -> AiSignaturePattern:
"""Build a trailer pattern for a structured ``Key: value`` line.

The match is case-insensitive and anchors the key at the start of a line.
"""
raw = rf"^{re.escape(key)}:\s*{value_pattern}\s*$"
return AiSignaturePattern(
regex=re.compile(raw, re.IGNORECASE | re.MULTILINE),
kind="trailer",
description=description or f"``{key}:`` trailer",
)


def _body_marker(pattern: str, description: str = "") -> AiSignaturePattern:
"""Build a free-text body marker pattern."""
return AiSignaturePattern(
regex=re.compile(pattern, re.MULTILINE),
kind="body_marker",
description=description,
)


# ---------------------------------------------------------------------------
# Known tool signatures
# ---------------------------------------------------------------------------

# --- Anthropic Claude Code / Claude CLI ---
CLAUDE_CODE = KnownAiTool(
name="Claude Code",
patterns=[
# Standard Co-authored-by trailer added by Claude Code.
# When an email is present, anchor to known AI noreply addresses
# to avoid false positives with human co-authors named Claude.
_trailer(
"Co-authored-by",
r"Claude(?: Code)?"
r"(?:\s*<(?:noreply@anthropic\.com"
r"|\d+\+Claude@users\.noreply\.github\.com)>)?",
"``Co-authored-by: Claude`` trailer",
),
# Assisted-by trailer (Linux kernel style, with optional tool list)
_trailer(
"Assisted-by",
r"Claude:\S+(?:\s+\S+)*",
"``Assisted-by: Claude:<model> [tools]`` trailer",
),
# Body marker: generated-with notice
_body_marker(
r"🤖\s*Generated\s+(?:with|by)\s+\[?Claude",
"``🤖 Generated with Claude`` body marker",
),
# Session ID trailer (Claude Code sometimes adds this)
_trailer("Claude-Session", r"\S+", "``Claude-Session:`` trailer"),
# Workflow ID trailer
_trailer("Claude-Workflow", r"\S+", "``Claude-Workflow:`` trailer"),
],
)

# --- GitHub Copilot ---
COPILOT = KnownAiTool(
name="GitHub Copilot",
patterns=[
_trailer(
"Co-authored-by",
r"Copilot"
r"(?:\s*<\d+\+Copilot@users\.noreply\.github\.com>)?",
"``Co-authored-by: Copilot`` trailer",
),
],
)

# --- OpenAI Codex ---
CODEX = KnownAiTool(
name="OpenAI Codex",
patterns=[
_trailer(
"Co-authored-by",
r"Codex\s*(?:<[^>]*>)?",
"``Co-authored-by: Codex`` trailer",
),
],
)

# --- Gemini (Google) ---
GEMINI = KnownAiTool(
name="Gemini",
patterns=[
_trailer(
"Co-authored-by",
r"Gemini\s*(?:<[^>]*>)?",
"``Co-authored-by: Gemini`` trailer",
),
],
)

# --- Cursor ---
CURSOR = KnownAiTool(
name="Cursor",
patterns=[
_trailer(
"Co-authored-by",
r"Cursor\s*(?:<[^>]*>)?",
"``Co-authored-by: Cursor`` trailer",
),
],
)

# --- Devin ---
DEVIN = KnownAiTool(
name="Devin",
patterns=[
_trailer(
"Co-authored-by",
r"Devin\s*(?:<[^>]*>)?",
"``Co-authored-by: Devin`` trailer",
),
],
)
Comment on lines +159 to +169

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Devin AI Cognition commit co-authored-by bot email address

💡 Result:

Devin AI, developed by Cognition, uses a Co-authored-by trailer in its git commit messages to provide attribution for changes made by the AI agent [1][2]. Regarding the email address associated with this trailer, official documentation for Devin notes a correction in its changelog: the system was updated to use the correct GitHub App bot email address instead of the previously used noreply@cognition.ai address [3]. The standard format used for the trailer is: Co-authored-by: Devin devin@cognition.ai While earlier or alternative configurations may have utilized other patterns, current documentation emphasizes the transition to official GitHub App bot integrations for improved attribution and compatibility [3]. Depending on the specific integration setup (such as the Devin GitHub App), the trailer may reference the official GitHub App bot identity (e.g., devin-ai-integration[bot]) to ensure clear provenance within repository histories [4]. Users are encouraged to check their specific repository's Devin integration documentation for the most accurate, environment-specific attribution string [5][3].

Citations:


Restrict DEVIN to a bot identity

Devin\s*(?:<[^>]*>)? still matches any human co-author named Devin with any email, so Co-authored-by: Devin <devin@company.com> is classified as AI and rejected in forbid mode. Anchor this to Devin’s bot email/identity instead of a bare name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@commit_check/ai_signatures_data.py` around lines 159 - 169, The DEVIN
signature is too broad because the current Co-authored-by matcher in
ai_signatures_data.py accepts any person named Devin, so tighten the KnownAiTool
pattern to match Devin’s specific bot identity/email instead of a bare name.
Update the DEVIN entry and its _trailer matcher so only the intended bot
co-author format is recognized, and ensure human co-authors named Devin no
longer match.


# --- Aider ---
AIDER = KnownAiTool(
name="Aider",
patterns=[
_trailer(
"Co-authored-by",
r"Aider\s*(?:<[^>]*>)?",
"``Co-authored-by: Aider`` trailer",
),
# aider appends "(aider)" to the author name
_trailer(
"Co-authored-by",
r"[^<]+\(aider\)\s*(?:<[^>]*>)?",
"``Co-authored-by: ... (aider)`` trailer",
),
],
)

# --- Windsurf (Codeium) ---
WINDSURF = KnownAiTool(
name="Windsurf",
patterns=[
_trailer(
"Co-authored-by",
r"Windsurf\s*(?:<[^>]*>)?",
"``Co-authored-by: Windsurf`` trailer",
),
],
)

# --- Tabby ---
TABBY = KnownAiTool(
name="Tabby",
patterns=[
_trailer(
"Co-authored-by",
r"Tabby\s*(?:<[^>]*>)?",
"``Co-authored-by: Tabby`` trailer",
),
],
)

# --- Generic / catch-all AI patterns ---
GENERIC_AI = KnownAiTool(
name="Generic AI",
patterns=[
# Catch AI agent model identifiers in Co-authored-by
# (e.g. claude-sonnet-4, gpt-4-turbo, gemini-1.5-pro).
# A hyphenated model suffix is required so bare human first names
# ("Claude", "Gemini") are NOT flagged, regardless of the email.
_trailer(
"Co-authored-by",
r"(?:claude|gpt|gemini)[\w.]*-[\w.-]+(?:\s*<[^>]*>)?",
"``Co-authored-by`` with AI model name",
),
# Catch Assisted-by trailer (Linux kernel style) regardless of agent,
# with optional trailing tool list.
_trailer(
"Assisted-by",
r"\S+:\S+(?:\s+\S+)*",
"``Assisted-by: <tool>:<model> [tools]`` trailer (kernel style)",
),
# Catch common body markers
_body_marker(
r"^Generated\s+(?:by|with)\s+(?:AI|artificial intelligence)",
"``Generated by AI`` body marker",
),
],
)

# ---------------------------------------------------------------------------
# Master registry — ordered by specificity (most specific first)
# ---------------------------------------------------------------------------

#: All known AI tools, ordered so that more specific patterns are checked first.
ALL_KNOWN_TOOLS: list[KnownAiTool] = [
CLAUDE_CODE,
COPILOT,
CODEX,
GEMINI,
CURSOR,
DEVIN,
AIDER,
WINDSURF,
TABBY,
GENERIC_AI,
]
1 change: 1 addition & 0 deletions commit_check/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def validate_message(
"allow_empty_commits",
"allow_fixup_commits",
"allow_wip_commits",
"ai_attribution",
]
return _run_checks(check_names, context, cfg)

Expand Down
Loading
Loading