From 208a079f4230edd520f5aa92288ae48247340910 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 19 Apr 2026 17:14:54 -0500 Subject: feat(hooks): shared _common.py helpers + systemMessage AI-attribution warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidates stdin-parse and response-emit across the two confirm hooks into `hooks/_common.py` (stdlib-only, sibling symlinked alongside the hooks it serves). Net ~28 lines less duplication. Adds a `systemMessage` banner alongside the confirmation modal when the commit message or PR title/body contains AI-attribution patterns: - Co-Authored-By: Claude|Anthropic|GPT|AI trailers - 🤖 robot emoji - "Generated with Claude Code" / similar footers - "Created with …" / "Assisted by …" variants Scanning targets structural leak patterns only — bare mentions of "Claude" or "Anthropic" in diff context don't fire, so discussing the tools themselves in a commit message doesn't false-positive. Clean-room synthesis from GowayLee/cchooks (MIT) — specifically, the systemMessage-alongside-reason pattern and event-aware stdin helpers. --- hooks/_common.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 hooks/_common.py (limited to 'hooks/_common.py') diff --git a/hooks/_common.py b/hooks/_common.py new file mode 100644 index 0000000..d4bf520 --- /dev/null +++ b/hooks/_common.py @@ -0,0 +1,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 -- cgit v1.2.3