aboutsummaryrefslogtreecommitdiff
path: root/hooks/_common.py
blob: d4bf5206d32ce13532001ffa0804c1fdbcd4324c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
"""Shared helpers for Claude Code PreToolUse confirmation hooks.

Not a hook itself — imported by sibling scripts in ~/.claude/hooks/
(installed as symlinks by `make install-hooks`). Python resolves imports
relative to the invoked script's directory, so sibling symlinks just work.

Provides:
  read_payload()      → dict parsed from stdin (empty dict on failure)
  respond_ask(...)    → emit a PreToolUse permissionDecision=ask response
  scan_attribution()  → detect AI-attribution patterns in commit/PR text

AI-attribution scanning targets structural leak patterns (trailers,
footers, robot emoji) — NOT bare mentions of 'Claude' or 'Anthropic',
which are legitimate words and would false-positive on diffs discussing
the tools themselves.
"""

import json
import re
import sys
from typing import Optional


ATTRIBUTION_PATTERNS: list[tuple[str, str]] = [
    (r"Co-Authored-By:\s*(?:Claude|Anthropic|GPT|AI\b|an? LLM)",
     "Co-Authored-By trailer crediting an AI"),
    (r"🤖",
     "robot emoji (🤖)"),
    (r"Generated (?:with|by) (?:Claude|Anthropic|AI|an? LLM)",
     "'Generated with AI' footer"),
    (r"Created (?:with|by) (?:Claude|Anthropic|AI|an? LLM)",
     "'Created with AI' footer"),
    (r"Assisted by (?:Claude|Anthropic|AI|an? LLM)",
     "'Assisted by AI' credit"),
    (r"\[\s*(?:Claude|AI|LLM)\s*(?:Code)?\s*\]",
     "[Claude] / [AI] bracketed tag"),
]


def read_payload() -> dict:
    """Parse tool-call JSON from stdin. Return {} on any parse failure."""
    try:
        return json.loads(sys.stdin.read())
    except (json.JSONDecodeError, ValueError):
        return {}


def respond_ask(reason: str, system_message: Optional[str] = None) -> None:
    """Emit a PreToolUse response asking the user to confirm.

    `reason` fills the modal body (permissionDecisionReason).
    `system_message`, if set, surfaces a secondary banner/warning to the
    user in a slot distinct from the modal.
    """
    output: dict = {
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "ask",
            "permissionDecisionReason": reason,
        }
    }
    if system_message:
        output["systemMessage"] = system_message
    print(json.dumps(output))


def scan_attribution(text: str) -> list[str]:
    """Return human-readable descriptions of any AI-attribution hits."""
    if not text:
        return []
    hits: list[str] = []
    for pattern, description in ATTRIBUTION_PATTERNS:
        if re.search(pattern, text, re.IGNORECASE):
            hits.append(description)
    return hits