diff --git a/.claude/hooks/notify.py b/.claude/hooks/notify.py new file mode 100644 index 0000000..ead3b33 --- /dev/null +++ b/.claude/hooks/notify.py @@ -0,0 +1,97 @@ +"""Stop / Notification / PreToolUse(AskUserQuestion) hook: 作業完了・許可待ち・質問を ntfy でスマホに通知する. + +送信先トピックは環境変数 NTFY_TOPIC で指定する(未設定なら何もしない).トピック名は +知っていれば誰でも購読・送信できるパスワード相当の値のため,リポジトリには書かず +.claude/settings.local.json(gitignore 済み)の env 等で各自設定する. +自前サーバー等へ送る場合は NTFY_SERVER で送信先を変えられる(既定: https://ntfy.sh). + +settings.json では async で配線しているため,本フックの成否は Claude Code の動作に影響しない. +送信に失敗しても黙って終了する.外部サービスを経由するため,本文にコードや会話の内容は含めない. +""" + +import json +import os +import subprocess +import sys +import urllib.request + +DEFAULT_SERVER = "https://ntfy.sh" +SEND_TIMEOUT_SEC = 10 +GIT_TIMEOUT_SEC = 5 +# ntfy の優先度(3 が既定.4 は通知音・バイブが強調される) +PRIORITY_NEEDS_INPUT = 4 + + +def build_notification(data: dict) -> dict | None: + """イベントから ntfy の通知内容(本文・タグ・優先度)を決める.通知不要なら None を返す.""" + event = data.get("hook_event_name") + if event == "Stop": + return {"message": "処理が完了しました", "tags": ["white_check_mark"]} + if event == "PreToolUse" and data.get("tool_name") == "AskUserQuestion": + return {"message": "質問への回答を待っています", "tags": ["question"], "priority": PRIORITY_NEEDS_INPUT} + if event == "Notification": + if data.get("notification_type") == "permission_prompt": + return {"message": "ツール実行の許可を待っています", "tags": ["lock"], "priority": PRIORITY_NEEDS_INPUT} + return {"message": "入力を待っています", "tags": ["speech_balloon"], "priority": PRIORITY_NEEDS_INPUT} + return None + + +def project_name(cwd: str) -> str: + """通知に出すプロジェクト名を返す. + + ホスト OS と dev container でフォルダ名が異なっても同じ名前になるよう, + origin リモートの URL 末尾(https://github.com/owner/repo.git・git@github.com:owner/repo.git + のどちらでも repo)を使う.リモートが無い・git が使えない場合はフォルダ名を返す. + """ + folder = os.path.basename(os.path.normpath(cwd)) + try: + result = subprocess.run( + ["git", "-C", cwd, "remote", "get-url", "origin"], + capture_output=True, + text=True, + timeout=GIT_TIMEOUT_SEC, + ) + except (OSError, subprocess.SubprocessError): + return folder + url = result.stdout.strip().rstrip("/") + if result.returncode != 0 or not url: + return folder + name = url.replace(":", "/").rsplit("/", 1)[-1].removesuffix(".git") + return name or folder + + +def main() -> None: + topic = os.environ.get("NTFY_TOPIC") + if not topic: + return + + try: + data = json.loads(sys.stdin.buffer.read().decode("utf-8")) + except (ValueError, UnicodeDecodeError): + return + + notification = build_notification(data) + if notification is None: + return + + # 複数プロジェクトを並行して動かしても区別できるよう,タイトルにプロジェクト名を入れる + project = project_name(data.get("cwd") or os.getcwd()) + payload = {"topic": topic, "title": f"Claude Code ({project})", **notification} + + # JSON で送る(ヘッダー送信だと日本語のタイトルが化けるため) + server = os.environ.get("NTFY_SERVER", DEFAULT_SERVER).rstrip("/") + request = urllib.request.Request( + server, + data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=SEND_TIMEOUT_SEC): + pass + except OSError: # URLError・HTTPError・タイムアウトはいずれも OSError の派生 + pass + + +if __name__ == "__main__": + main() diff --git a/.claude/settings.json b/.claude/settings.json index 7f2d550..e73683c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -4,8 +4,6 @@ "Bash(gh pr merge:*)" ] }, - "inputNeededNotifEnabled": true, - "agentPushNotifEnabled": true, "hooks": { "PreToolUse": [ { @@ -16,6 +14,39 @@ "command": "python .claude/hooks/restrict_repo_access.py" } ] + }, + { + "matcher": "AskUserQuestion", + "hooks": [ + { + "type": "command", + "command": "python .claude/hooks/notify.py", + "async": true + } + ] + } + ], + "Notification": [ + { + "matcher": "permission_prompt|elicitation_dialog|elicitation_url_dialog", + "hooks": [ + { + "type": "command", + "command": "python .claude/hooks/notify.py", + "async": true + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "python .claude/hooks/notify.py", + "async": true + } + ] } ] } diff --git "a/docs/01_GUIDE/GUIDE_01_\343\203\227\343\203\255\343\202\270\343\202\247\343\202\257\343\203\210\347\253\213\343\201\241\344\270\212\343\201\222\343\203\225\343\203\255\343\203\274.md" "b/docs/01_GUIDE/GUIDE_01_\343\203\227\343\203\255\343\202\270\343\202\247\343\202\257\343\203\210\347\253\213\343\201\241\344\270\212\343\201\222\343\203\225\343\203\255\343\203\274.md" index 250a969..758b7ca 100644 --- "a/docs/01_GUIDE/GUIDE_01_\343\203\227\343\203\255\343\202\270\343\202\247\343\202\257\343\203\210\347\253\213\343\201\241\344\270\212\343\201\222\343\203\225\343\203\255\343\203\274.md" +++ "b/docs/01_GUIDE/GUIDE_01_\343\203\227\343\203\255\343\202\270\343\202\247\343\202\257\343\203\210\347\253\213\343\201\241\344\270\212\343\201\222\343\203\225\343\203\255\343\203\274.md" @@ -59,10 +59,11 @@ - **GitHub リポジトリのセキュリティ設定**: GitHub の Settings → Code security にある **Dependabot alerts**(既知脆弱性の検出."Vulnerabilities" として表示される)と **Dependabot security updates** はリポジトリごとに既定で OFF のため,リポジトリを作成したら必ず有効化する.モードに関わらず `/setup` が `gh api` で有効化・検証する(コマンド・検証・トラブル対応は `.claude/skills/setup/reference.md`「GitHub リポジトリのセキュリティ設定」).`/setup` 時点でリポジトリが無い場合は,`ENV_03_管理者用環境構築手順.md` に転記した同じコマンドをリポジトリ作成後に実行する. - **依存バージョン更新(Dependabot version updates)**: 上記とは別に,依存パッケージを定期的に最新化する PR を作らせるため,`/setup` が技術スタックに合わせて `.github/dependabot.yml` を必ず生成する(週 1 回・マイナー/パッチをまとめる・`[update]` プレフィックス.エコシステム対応表と雛形は `.claude/skills/setup/reference.md`「依存バージョン更新の設定」).Dependabot が作る PR と alert の処理は `/deps-update` で行う(ゲートを満たす PR を自動マージし,メジャー更新等は分析付きで報告.GUIDE_02「コミットルール」の例外). - **Claude Code の権限設定(`.claude/settings.json`)**: テンプレート同梱の `permissions.allow`(`Bash(gh pr merge:*)`)は,auto mode で ops-runner に `/commit merge`・`/deps-update` のマージを実行させるために必要なので削除しない(変更はセッション再起動後に反映).マージが分類器に止められた場合の切り分けと対処は `.claude/skills/commit/reference.md`「gh コマンドが実行される前にブロックされた場合」を参照. -- **スマホへのプッシュ通知(任意)**: テンプレート同梱の `.claude/settings.json` は `inputNeededNotifEnabled`(許可待ち・質問など入力待ちで通知)と `agentPushNotifEnabled`(長い作業の完了など,Claude の判断で通知)を有効にしている.通知は Claude Code の Remote Control 経由で届くため,受け取りたい人は各自で以下を行う(Remote Control を使わない人には影響しない). - - Claude モバイルアプリに,Claude Code と同じ claude.ai アカウントでログインする(API キーでのログインは Remote Control 非対応) - - Remote Control の自動接続(`remoteControlAtStartup`)はプロジェクトの settings.json では有効化できない仕様のため,Claude Code を動かす環境ごと(ホスト OS・dev container それぞれ)に `/config` で一度オンにする.dev container ではコンテナ内の `~/.claude` を永続化していないと再ビルドで設定が消える - - dev container で `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` を設定していると Remote Control が使えない +- **スマホへのプッシュ通知(任意)**: テンプレート同梱の `.claude/settings.json` は,作業完了(`Stop`.毎ターン)・許可待ち(`Notification`)・質問(`PreToolUse` の `AskUserQuestion`)で `.claude/hooks/notify.py` を呼び,[ntfy](https://ntfy.sh) 経由でスマホに通知する(ホスト OS・dev container のどちらでも動く).送信先トピックが未設定なら何もしないため,受け取りたい人だけ以下を行う. + - スマホに ntfy アプリを入れ,推測されにくいトピック名(例: `claude-` +ランダムな英数字)を購読する.トピック名を知っていれば誰でも購読・送信できるため,パスワードと同様に扱いリポジトリには書かない + - iPhone でアプリを開くと届いているのにプッシュ通知が出ない場合は,トピックを削除してアプリを再起動し,購読し直す(ntfy の既知の問題.購読し直すと通知の登録がやり直される) + - プロジェクトの `.claude/settings.local.json`(gitignore 済み)に `{ "env": { "NTFY_TOPIC": "<トピック名>" } }` を書く.ワークスペースごと dev container に共有されるため,プロジェクトごとに 1 回でよい(反映されない場合はセッションを再起動する) + - 外部サービスを経由するため,通知には「Claude Code (リポジトリ名)」(origin リモートが無ければフォルダ名)と定型文だけを送り,コードや会話の内容は送らない - **成果物**: - `ENV_02_環境構築手順.md` — メンバーの参加時や環境の再構築時に使う手順 - `ENV_03_管理者用環境構築手順.md` — プロジェクト作成時に一度だけ行う初期設定(リポジトリ作成,GitHub リポジトリのセキュリティ設定,外部サービスの設定等)