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
3 changes: 2 additions & 1 deletion .claude/hooks/block_config_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
"""
import sys
import json
from common import load_hook_input

data = json.load(sys.stdin)
data = load_hook_input()
Comment on lines 11 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused json import.

The json module is imported but no longer used after refactoring to use load_hook_input().

🧹 Proposed fix
 import sys
-import json
 from common import load_hook_input
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/hooks/block_config_edit.py around lines 11 - 15, Remove the unused
import of the json module: delete the line importing json at the top of
.claude/hooks/block_config_edit.py since the code now uses load_hook_input()
(function referenced as load_hook_input) and no other code uses json; keep the
import of load_hook_input and the existing data = load_hook_input() line intact.

tool_input = data.get("tool_input", {}) or {}
file_path = tool_input.get("file_path") or tool_input.get("path") or ""

Expand Down
5 changes: 3 additions & 2 deletions .claude/hooks/block_dangerous_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
import sys
import json
import re
from common import load_hook_input, get_command

data = json.load(sys.stdin)
cmd = (data.get("tool_input", {}) or {}).get("command") or ""
data = load_hook_input()
cmd = get_command(data)
Comment on lines 8 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused json import.

The json module is imported but no longer used after refactoring to use load_hook_input() from common.

🧹 Proposed fix
 import sys
-import json
 import re
 from common import load_hook_input, get_command
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import sys
import json
import re
from common import load_hook_input, get_command
data = json.load(sys.stdin)
cmd = (data.get("tool_input", {}) or {}).get("command") or ""
data = load_hook_input()
cmd = get_command(data)
import sys
import re
from common import load_hook_input, get_command
data = load_hook_input()
cmd = get_command(data)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/hooks/block_dangerous_commands.py around lines 8 - 14, Remove the
unused json import from the top-level imports; edit the import block so it only
imports sys, re, and the common helpers (remove the line "import json"), leaving
load_hook_input and get_command usage unchanged (symbols to check:
load_hook_input, get_command, data, cmd).


if not cmd.strip():
sys.exit(0)
Expand Down
6 changes: 3 additions & 3 deletions .claude/hooks/block_git_no_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import sys
import json
import shlex
from common import load_hook_input, get_command

# Read input from Claude
data = json.load(sys.stdin)
cmd = (data.get("tool_input", {}) or {}).get("command") or ""
data = load_hook_input()
cmd = get_command(data)
Comment on lines 2 to +8

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused json import.

The json module is imported but no longer used after refactoring to use load_hook_input() and get_command().

🧹 Proposed fix
 import sys
-import json
 import shlex
 from common import load_hook_input, get_command
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import sys
import json
import shlex
from common import load_hook_input, get_command
# Read input from Claude
data = json.load(sys.stdin)
cmd = (data.get("tool_input", {}) or {}).get("command") or ""
data = load_hook_input()
cmd = get_command(data)
import sys
import shlex
from common import load_hook_input, get_command
data = load_hook_input()
cmd = get_command(data)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/hooks/block_git_no_verify.py around lines 2 - 8, Remove the unused
json import from the top of the file; locate the import line that currently
reads "import json" alongside "import sys" and "import shlex" and delete it so
only required modules remain, since load_hook_input() and get_command() are used
and json is no longer referenced.

tokens = shlex.split(cmd) if cmd else []

if not tokens:
Expand Down
139 changes: 139 additions & 0 deletions .claude/hooks/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""Shared utilities for Claude Code hooks.

Consolidates common patterns used across hook files:
- JSON input parsing from stdin
- Tool context extraction
- Output formatting (headers, sections, status)
- Git operations
- Package manager detection
"""
import sys
import json
import re
import subprocess
from pathlib import Path
from typing import Optional


def load_hook_input() -> dict:
"""Load and return JSON input from stdin."""
return json.load(sys.stdin)


def parse_tool_context(data: dict) -> tuple:
"""Extract standard tool context fields.

Returns:
tuple: (tool_name, tool_input, tool_response)
"""
tool_name = data.get("tool_name", "")
tool_input = data.get("tool_input", {}) or {}
tool_response = data.get("tool_response", {}) or {}
return tool_name, tool_input, tool_response


def get_command(data: dict) -> str:
"""Extract command string from hook input data."""
tool_input = data.get("tool_input", {}) or {}
return (tool_input.get("command") or "").strip()


def is_bash_command(tool_name: str) -> bool:
"""Check if the tool is the Bash tool."""
return tool_name == "Bash"


def is_help_command(command: str) -> bool:
"""Check if command is a help/dry-run command."""
return "--help" in command or "-h" in command


def extract_pr_url(text: str) -> Optional[tuple]:
"""Extract PR URL and components from text.

Returns:
Optional[tuple]: (pr_url, owner, repo, pr_number) or None
"""
pattern = r"https://github\.com/([^/]+)/([^/]+)/pull/(\d+)"
match = re.search(pattern, text)
if match:
return match.group(0), match.group(1), match.group(2), match.group(3)
return None


# ============================================================================
# Output formatting
# ============================================================================

def print_header(message: str, width: int = 60) -> None:
"""Print formatted header with separators to stderr."""
print("", file=sys.stderr, flush=True)
print("=" * width, file=sys.stderr, flush=True)
print(message, file=sys.stderr, flush=True)
print("=" * width, file=sys.stderr, flush=True)


def print_footer(width: int = 60) -> None:
"""Print footer separator to stderr."""
print("", file=sys.stderr, flush=True)
print("=" * width, file=sys.stderr, flush=True)


def print_section(title: str, width: int = 40) -> None:
"""Print section header to stderr."""
print("", file=sys.stderr, flush=True)
print(f"## {title}", file=sys.stderr, flush=True)
print("-" * width, file=sys.stderr, flush=True)


def print_status(message: str) -> None:
"""Print status message to stderr."""
print(message, file=sys.stderr, flush=True)


# ============================================================================
# Git operations
# ============================================================================

def get_git_root() -> Optional[Path]:
"""Get repository root directory, return None if not a git repo."""
try:
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
capture_output=True, text=True, timeout=10
)
root = result.stdout.strip()
return Path(root) if root and result.returncode == 0 else None
except (subprocess.TimeoutExpired, FileNotFoundError):
return None


def get_changed_files(ref: str = "HEAD", cwd: Optional[str] = None) -> list:
"""Get list of changed files for given ref."""
try:
result = subprocess.run(
["git", "diff-tree", "--no-commit-id", "--name-only", "-r", ref],
capture_output=True, text=True, timeout=5,
cwd=cwd,
)
if result.returncode == 0 and result.stdout.strip():
return result.stdout.strip().split("\n")
return []
except (subprocess.TimeoutExpired, OSError):
return []


# ============================================================================
# Package manager detection
# ============================================================================

def detect_package_manager(root: Path) -> str:
"""Detect package manager from lock files."""
if (root / "pnpm-lock.yaml").exists():
return "pnpm"
if (root / "yarn.lock").exists():
return "yarn"
if (root / "bun.lockb").exists() or (root / "bun.lock").exists():
return "bun"
return "npm"
10 changes: 4 additions & 6 deletions .claude/hooks/post_commit_adr_reminder.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@
import subprocess
import re
import shlex
from common import load_hook_input, parse_tool_context, is_bash_command

data = json.load(sys.stdin)
data = load_hook_input()
tool_name, tool_input, tool_response = parse_tool_context(data)

tool_name = data.get("tool_name", "")
tool_input = data.get("tool_input", {}) or {}
tool_response = data.get("tool_response", {}) or {}

if tool_name != "Bash":
if not is_bash_command(tool_name):
sys.exit(0)

command = tool_input.get("command", "").strip()
Expand Down
3 changes: 2 additions & 1 deletion .claude/hooks/post_edit_auto_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
import subprocess
import shutil
from pathlib import Path
from common import load_hook_input

data = json.load(sys.stdin)
data = load_hook_input()
tool_input = data.get("tool_input", {}) or {}
file_path = tool_input.get("file_path") or tool_input.get("path") or ""

Expand Down
25 changes: 7 additions & 18 deletions .claude/hooks/post_git_push_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,19 @@
import subprocess
import re
import time
from common import (load_hook_input, parse_tool_context, is_bash_command,
print_header, print_footer, print_status)

# Read input from Claude
data = json.load(sys.stdin)
data = load_hook_input()
tool_name, tool_input, tool_response = parse_tool_context(data)

tool_name = data.get("tool_name", "")
tool_input = data.get("tool_input", {}) or {}
tool_response = data.get("tool_response", {}) or {}

# Bashツールでない場合はスキップ
if tool_name != "Bash":
if not is_bash_command(tool_name):
sys.exit(0)

# コマンドを取得
command = tool_input.get("command", "").strip()

# git push コマンドかどうかを判定
if not command.startswith("git push"):
sys.exit(0)

# --help や --dry-run は除外
if "--help" in command or "-h" in command or "--dry-run" in command or "-n" in command:
sys.exit(0)

Expand Down Expand Up @@ -62,10 +55,7 @@
if not is_success:
sys.exit(0)

print("", file=sys.stderr, flush=True)
print("=" * 60, file=sys.stderr, flush=True)
print("🚀 Push完了。GitHub Actions CIを確認中...", file=sys.stderr, flush=True)
print("=" * 60, file=sys.stderr, flush=True)
print_header("🚀 Push完了。GitHub Actions CIを確認中...")


def get_current_branch():
Expand Down Expand Up @@ -197,8 +187,7 @@ def watch_ci_run(run_id, timeout_seconds=300):
else:
print(f"\n⚠️ CI結果: {conclusion}", file=sys.stderr, flush=True)

print("", file=sys.stderr, flush=True)
print("=" * 60, file=sys.stderr, flush=True)
print_footer()

# PostToolUseフックは常に成功で終了(ブロックしない)
sys.exit(0)
55 changes: 17 additions & 38 deletions .claude/hooks/post_pr_ai_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,49 +12,37 @@
import subprocess
import shutil
import os
import re
from concurrent.futures import ThreadPoolExecutor, as_completed
from common import (load_hook_input, parse_tool_context, is_bash_command,
is_help_command, extract_pr_url, print_header, print_footer,
print_section, print_status)

# モデル設定(空文字列の場合はCLIのデフォルトモデルを使用)
CODEX_MODEL = "" # デフォルトモデルを使用(ChatGPTアカウント互換)
GEMINI_MODEL = "" # デフォルトモデルを使用

# Read input from Claude
data = json.load(sys.stdin)
data = load_hook_input()
tool_name, tool_input, tool_response = parse_tool_context(data)

tool_name = data.get("tool_name", "")
tool_input = data.get("tool_input", {}) or {}
tool_response = data.get("tool_response", {}) or {}

# Bashツールでない場合はスキップ
if tool_name != "Bash":
if not is_bash_command(tool_name):
sys.exit(0)

# コマンドを取得
command = tool_input.get("command", "").strip()

# gh pr create コマンドかどうかを厳密に判定(プレフィックス判定)
if not command.startswith("gh pr create"):
sys.exit(0)

# ヘルプコマンドは除外
if "--help" in command or "-h" in command:
if is_help_command(command):
sys.exit(0)

# ツール実行が成功したかチェック
stdout = tool_response.get("stdout", "")
stderr = tool_response.get("stderr", "")

# PR URLパターン(https://github.com/owner/repo/pull/123 形式)
pr_url_pattern = r"https://github\.com/[^/]+/[^/]+/pull/\d+"
combined_output = stdout + stderr

# PR URLを抽出
pr_url_match = re.search(pr_url_pattern, combined_output)
if not pr_url_match:
pr_info = extract_pr_url(combined_output)
if not pr_info:
sys.exit(0)

pr_url = pr_url_match.group(0)
pr_url = pr_info[0]

# 利用可能なAIツールを確認
has_codex = shutil.which("codex") is not None
Expand All @@ -76,18 +64,12 @@

**重要: 必ず日本語で回答してください。**"""

print("", file=sys.stderr, flush=True)
print("=" * 60, file=sys.stderr, flush=True)
print("🔍 PR作成完了。AIレビューを実行中...", file=sys.stderr, flush=True)
print(f"📎 PR: {pr_url}", file=sys.stderr, flush=True)
print("=" * 60, file=sys.stderr, flush=True)
print_header(f"🔍 PR作成完了。AIレビューを実行中...\n📎 PR: {pr_url}")


def run_codex_review():
"""Codexによるレビューを実行し、結果を返す"""
print("", file=sys.stderr, flush=True)
print("## 🤖 Codex Review", file=sys.stderr, flush=True)
print("-" * 40, file=sys.stderr, flush=True)
print_section("🤖 Codex Review")

codex_command = ["codex", "exec", "--sandbox", "read-only"]
if CODEX_MODEL:
Expand Down Expand Up @@ -125,9 +107,7 @@ def run_codex_review():

def run_gemini_review():
"""Geminiによるレビューを実行し、結果を返す"""
print("", file=sys.stderr, flush=True)
print("## ✨ Gemini Review", file=sys.stderr, flush=True)
print("-" * 40, file=sys.stderr, flush=True)
print_section("✨ Gemini Review")

try:
# マージベースを取得
Expand Down Expand Up @@ -291,13 +271,12 @@ def check_for_issues(review_text: str) -> bool:
print("", file=sys.stderr, flush=True)
print("⚠️ レビュー結果がないため、PRコメントはスキップします", file=sys.stderr, flush=True)

print("", file=sys.stderr, flush=True)
print("=" * 60, file=sys.stderr, flush=True)
print_footer()
if issues_found:
print("⚠️ AIレビュー完了 - 問題が検出されました。修正を検討してください。", file=sys.stderr, flush=True)
print_status("⚠️ AIレビュー完了 - 問題が検出されました。修正を検討してください。")
else:
print("✅ AIレビュー完了", file=sys.stderr, flush=True)
print("=" * 60, file=sys.stderr, flush=True)
print_status("✅ AIレビュー完了")
print_footer()

# PostToolUseフックは常に成功で終了(ブロックしない)
sys.exit(0)
Loading
Loading