diff --git a/.claude/commands/commit.md b/.claude/commands/commit.md index 6b2c025..be28522 100644 --- a/.claude/commands/commit.md +++ b/.claude/commands/commit.md @@ -22,25 +22,31 @@ 2. `git checkout -b {ブランチ名}` で作業ブランチを作成する 3. 以降のステップを続行する -## ステップ 2: docs/ 更新チェック +## ステップ 2: docs/ 更新漏れチェック -変更内容を確認し,以下に該当する場合は docs/ の更新が必要か確認する: +`.claude/commit-context.md` が存在し `docs_updated` が記録されている場合,このステップをスキップする. + +ファイルが存在しない場合(`/implement` を経由していない場合),変更内容を確認し,以下に該当するにもかかわらず docs/ が更新されていない場合は警告する: - API やデータ構造の変更 - 環境設定の変更 - 開発計画の進捗に関わる変更 - 規約やルールに関わる変更 -該当がある場合,必要な docs/ の更新を自動で行う. +該当がある場合,「⚠ docs/ の更新が必要かもしれません.確認してください.」と警告する. +更新自体は行わず,ユーザーの判断に委ねる. ## ステップ 3: コミットメッセージの生成 変更内容と $ARGUMENTS(指定がある場合)をもとに,GUIDE_04 の「コミットメッセージ」セクションに従ってコミットメッセージを生成する. -## ステップ 4: CLAUDE.md 更新 +## ステップ 4: CLAUDE.md 更新漏れチェック -変更内容に応じて CLAUDE.md の「開発進捗」セクションの更新が必要か判断し, -必要であれば自動で更新する. +`.claude/commit-context.md` が存在し `claude_md_updated` が記録されている場合,このステップをスキップする. + +ファイルが存在しない場合(`/implement` を経由していない場合),変更内容に応じて CLAUDE.md の「開発進捗」セクションの更新が必要か判断し, +更新されていない場合は「⚠ CLAUDE.md の開発進捗の更新が必要かもしれません.確認してください.」と警告する. +更新自体は行わず,ユーザーの判断に委ねる. ## ステップ 5: コミット @@ -48,6 +54,7 @@ 1. `git add` で関連ファイルをステージングする(CLAUDE.md の変更がある場合はそれも含める) 2. `git commit -m "{コミットメッセージ}"` でコミットする +3. `.claude/commit-context.md` が存在する場合は削除する ## ステップ 6: プッシュ・PR・マージ diff --git a/.claude/commands/implement.md b/.claude/commands/implement.md index 59788f5..61fda83 100644 --- a/.claude/commands/implement.md +++ b/.claude/commands/implement.md @@ -94,15 +94,52 @@ - **OK** → コミット・push・PR 作成に進みます - **NG** → 修正点を教えてください」 +## Phase 4: ドキュメント更新 (Documentation) + +リファクタリング完了後,実装内容に応じてドキュメントを更新する. + +### docs/ の更新チェック + +以下に該当する変更がある場合,docs/ 内の関連ドキュメントを更新する: + +- API やデータ構造の変更 +- 環境設定の変更 +- 開発計画の進捗に関わる変更 +- 規約やルールに関わる変更 + +### CLAUDE.md の更新 + +変更内容に応じて CLAUDE.md の「開発進捗」セクションを更新する. + +### コミットコンテキストの書き出し + +Phase 4 の結果を `.claude/commit-context.md` に書き出す. +このファイルは `/commit` が参照し,docs/CLAUDE.md の更新漏れチェックをスキップするために使う. + +```markdown +# commit-context +- docs_updated: true/false +- claude_md_updated: true/false +- note: (更新不要の理由や更新内容の要約) +``` + +### 更新がある場合のみ確認 + +ドキュメントを更新した場合は,更新内容をユーザーに提示する: + +「**Phase 4(ドキュメント更新)が完了しました.** + +{更新したドキュメントの一覧と変更内容の要約} + +確認をお願いします. +- **OK** → `/commit` でコミットできます +- **NG** → 修正点を教えてください」 + +更新不要と判断した場合は,確認を挟まず完了処理に進む. + ## 完了処理 (Finalization) -ユーザーの最終 OK を得た後: - -1. 全変更をステージングする(`git add` で関連ファイルを追加) -2. GUIDE_04 に従いコミットする(`[add]`/`[update]`/`[fix]` + 日本語メッセージ) -3. リモートにプッシュする(`git push origin {ブランチ名}`) -4. PR 作成を実行する(`gh pr create`) -5. CLAUDE.md の開発進捗の更新を提案する +「全フェーズが完了しました.`/commit` でコミットしてください.」 ## 注意事項 (Notes) diff --git a/.claude/hooks/restrict_repo_access.py b/.claude/hooks/restrict_repo_access.py index e2f8733..36bfe96 100644 --- a/.claude/hooks/restrict_repo_access.py +++ b/.claude/hooks/restrict_repo_access.py @@ -2,8 +2,13 @@ import json import os +import re +import shlex import sys +# Bash で検出する破壊的コマンド +DESTRUCTIVE_COMMANDS = {"rm", "rmdir", "mv", "cp", "chmod", "chown", "unlink"} + def get_target_path(tool_name: str, tool_input: dict) -> str | None: """ツールの種類に応じてチェック対象のパスを取得する.""" @@ -22,6 +27,49 @@ return real_target == real_base or real_target.startswith(real_base + os.sep) +def check_bash_command(command: str, cwd: str) -> str | None: + """Bash コマンド内の破壊的操作がリポジトリ外を対象としていないかチェックする. + + リポジトリ外のパスに対する破壊的コマンドを検出した場合、理由文字列を返す。 + 問題なければ None を返す。 + """ + # セミコロン、&&、|| やパイプで分割された各部分をチェック + parts = re.split(r"[;&|]+", command) + for part in parts: + part = part.strip() + if not part: + continue + try: + tokens = shlex.split(part) + except ValueError: + # クォートが閉じていない等のパースエラーは無視して続行 + continue + if not tokens: + continue + # コマンド名を取得(env, sudo 等のプレフィックスをスキップ) + cmd_name = None + for token in tokens: + if token in ("sudo", "env") or "=" in token: + continue + cmd_name = os.path.basename(token) + break + if cmd_name not in DESTRUCTIVE_COMMANDS: + continue + # 破壊的コマンドの引数からパスを抽出してチェック + for token in tokens[1:]: + if token.startswith("-"): + continue + # 絶対パスまたは .. を含むパスをチェック + if os.path.isabs(token) or ".." in token: + resolved = os.path.realpath(os.path.join(cwd, token)) + if not is_within_directory(resolved, cwd): + return ( + f"リポジトリ外への破壊的操作をブロックしました: " + f"`{cmd_name}` が `{token}` を対象としています" + ) + return None + + def deny(reason: str) -> None: """ブロック用の JSON を出力して終了する.""" result = { @@ -41,6 +89,15 @@ tool_input = data.get("tool_input", {}) cwd = data.get("cwd", os.getcwd()) + # Bash ツールの破壊的コマンドチェック + if tool_name == "Bash": + command = tool_input.get("command", "") + reason = check_bash_command(command, cwd) + if reason: + deny(reason) + sys.exit(0) + + # Read/Write/Edit/Glob/Grep のパスチェック target_path = get_target_path(tool_name, tool_input) # パスが指定されていない場合は許可(Glob/Grep の path 省略時など) diff --git a/.claude/settings.json b/.claude/settings.json index 548c175..3b0ffd7 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -2,7 +2,7 @@ "hooks": { "PreToolUse": [ { - "matcher": "Read|Write|Edit|Glob|Grep", + "matcher": "Read|Write|Edit|Glob|Grep|Bash", "hooks": [ { "type": "command", diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4419375 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# /implement が /commit に渡すコンテキストファイル +.claude/commit-context.md