aboutsummaryrefslogtreecommitdiff
path: root/hooks/_common.py
diff options
context:
space:
mode:
Diffstat (limited to 'hooks/_common.py')
-rw-r--r--hooks/_common.py75
1 files changed, 75 insertions, 0 deletions
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