Closed9

Claude CodeのHooksで詰まったメモ & Discordに通知を出してDiscordから対話できるようにする(Windows WSL VSCode)

sh11235sh11235

最初の上手くいったサンプル

$ claude -v
1.0.40 (Claude Code)

~/.claude/settings.json

{
  "model": "opus",
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$YOURHOME/bin/claude-notify.sh complete '作業が完了しました'",
            "description": "Notify when Claude Code finishes work"
          }
        ]
      }
    ],
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$YOURHOME/bin/claude-notify.sh waiting 'ユーザーの応答を待っています'",
            "description": "Notify when Claude Code is waiting for user input"
          }
        ]
      }
    ]
  }
}

~/bin/claude-notify.sh

#!/bin/bash

# Claude Code notification script
# Usage: claude-notify.sh <type> [message]

TYPE=$1
MESSAGE=${2:-"Claude Code notification"}

# 通知方法の自動検出
notify() {
    local title=$1
    local message=$2
    local urgency=${3:-normal}
    local icon=${4:-dialog-information}
    
    # notify-sendが利用可能な場合
    if command -v notify-send &> /dev/null; then
        notify-send "$title" "$message" -i "$icon" -u "$urgency"
    # macOSの場合
    elif command -v osascript &> /dev/null; then
        osascript -e "display notification \"$message\" with title \"$title\""
    # WSL環境の場合(PowerShell経由)
    elif command -v powershell.exe &> /dev/null; then
        powershell.exe -Command "
            Add-Type -AssemblyName System.Windows.Forms
            \$notification = New-Object System.Windows.Forms.NotifyIcon
            \$notification.Icon = [System.Drawing.SystemIcons]::Information
            \$notification.BalloonTipIcon = 'Info'
            \$notification.BalloonTipTitle = '$title'
            \$notification.BalloonTipText = '$message'
            \$notification.Visible = \$true
            \$notification.ShowBalloonTip(5000)
            Start-Sleep -Seconds 5
            \$notification.Dispose()
        "
    else
        # フォールバック: ターミナルに表示
        echo "[$title] $message" >&2
    fi
}

case "$TYPE" in
    "complete")
        # 作業完了通知
        notify "Claude Code" "作業が完了しました: $MESSAGE" "normal" "dialog-information"
        ;;
    "waiting")
        # 入力待ち通知(重要度高)
        notify "Claude Code" "あなたの入力を待っています: $MESSAGE" "critical" "dialog-warning"
        
        # 音声通知(利用可能な場合)
        if command -v paplay &> /dev/null; then
            # システムサウンドを再生
            if [ -f "/usr/share/sounds/freedesktop/stereo/message.oga" ]; then
                paplay /usr/share/sounds/freedesktop/stereo/message.oga 2>/dev/null
            fi
        elif command -v powershell.exe &> /dev/null; then
            # WSL環境でビープ音
            powershell.exe -Command "[console]::beep(1000,500)"
        else
            # 代替: ビープ音
            echo -e '\a'
        fi
        ;;
    *)
        # デフォルト通知
        notify "Claude Code" "$MESSAGE" "normal" "dialog-information"
        ;;
esac
sh11235sh11235

Claude Codeのバージョン

claudeで起動して、/status
を打つと

IDE Integration • /config
   ✔  Connected to VS Code extension
    L Installed VS Code extension version 1.0.40 (server version: 1.0.34) 

と出ていた。この不一致はVSCodeのリロードで解消した。
Claude自体のバージョンはさっき上げたばかりなので問題無いはず。

sh11235sh11235

Claude Code自身がHooksを知っていそうだったので適当に書かせたら、
公式ドキュメント https://docs.anthropic.com/en/docs/claude-code/hooks
とは違うパスにファイルを設置しだした。

ユーザーホームの設定ファイルを ~/.config/claude-code/settings.json に作っていた。
~/.claude/settings.json が正しい。
また、設定ファイルの記法も誤っていたので、↑のリファレンスを読ませて書き直させた(最初の投稿のファイル)。

sh11235sh11235

Discord通知について、好きに書かせるとPythonでプロジェクトを書き始めた
以下はドキュメント

Claude Code Discord通知セットアップガイド

概要

Claude CodeからのDiscord通知とリアクション制御を設定するためのガイドです。

セットアップ手順

1. Discord Webhookの作成

  1. Discordサーバーの設定を開く
  2. 「連携サービス」→「ウェブフック」を選択
  3. 「新しいウェブフック」をクリック
  4. 名前を設定(例: Claude Code Notifications)
  5. WebhookのURLをコピー

2. Discord Botの作成

  1. Discord Developer Portalにアクセス
  2. 「New Application」をクリック
  3. アプリケーション名を入力(例: Claude Code Bot)
  4. 「Bot」セクションに移動
  5. 「Add Bot」をクリック
  6. Bot TokenをコピーしてRESETを押さないように注意

3. Botの権限設定

  1. 「Bot」セクションで以下を確認:
    • 「Public Bot」のチェックを外す(プライベート使用の場合)
    • 「Requires OAuth2 Code Grant」のチェックを外す
  2. 「OAuth2」→「URL Generator」に移動
  3. Scopesで「bot」を選択
  4. Bot Permissionsで以下を選択:
    • View Channels
    • Send Messages
    • Read Message History
    • Add Reactions
    • Manage Messages
  5. 生成されたURLをコピーしてブラウザで開く
  6. Botを招待するサーバーを選択

4. 設定ファイルの作成

cp ~/.claude/discord-config.example.json ~/.claude/discord-config.json

設定ファイルを編集:

{
    "webhook_url": "https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN",
    "bot_token": "YOUR_BOT_TOKEN",
    "channel_id": "YOUR_CHANNEL_ID",
    "allowed_users": ["YOUR_DISCORD_USER_ID"],
    "reaction_timeout": 300,
    "reactions": {
        "approve": "✅",
        "deny": "❌",
        "pause": "⏸️"
    }
}

5. Discord IDの取得方法

  1. Discord設定で「詳細設定」→「開発者モード」を有効化
  2. チャンネルを右クリック→「IDをコピー」でchannel_idを取得
  3. ユーザー名を右クリック→「IDをコピー」でuser_idを取得

6. Botの起動

# バックグラウンドで起動
nohup claude-discord-bot > ~/logs/claude-discord-bot.log 2>&1 &

# または、systemdサービスとして設定(推奨)

動作確認

テスト通知を送信

# 通常の通知
~/bin/claude-notify.sh complete "テスト作業が完了しました"

# 権限要求通知(Discord通知になる)
~/bin/claude-notify.sh permission "sudo apt update を実行しようとしています"

# 入力待ち通知(Discord通知になる)
~/bin/claude-notify.sh waiting "ファイル名を入力してください"

Discordでの操作

  • ✅: 承認(approved)
  • ❌: 拒否(denied)
  • 💬: 確認(acknowledged)

トラブルシューティング

通知が届かない場合

  1. Discord設定ファイルが正しく設定されているか確認
  2. Webhook URLが有効か確認
  3. jqコマンドがインストールされているか確認: sudo apt install jq

Botが反応しない場合

  1. Botがオンラインか確認
  2. Bot TokenとChannel IDが正しいか確認
  3. Botに必要な権限があるか確認
  4. allowed_usersにあなたのDiscord IDが含まれているか確認

ログの確認

# Bot のログ
tail -f ~/logs/claude-discord-bot.log

# 一時ファイルの確認
ls -la /tmp/claude-discord-*

セキュリティ注意事項

  • Bot TokenとWebhook URLは秘密情報です。Gitにコミットしないでください
  • discord-config.jsonのパーミッションを制限することを推奨: chmod 600 ~/.claude/discord-config.json
  • allowed_usersで承認されたユーザーのみがBotを操作できるようにしてください
sh11235sh11235

機能したコード

bin/claude-discord-bot

#!/bin/bash
# Claude Discord Bot launcher script

PROJECT_DIR="$HOME/projects/claude-discord-bot"

# Run the bot using uv
cd "$PROJECT_DIR" && exec uv run python run.py "$@"

bin/claude-notify.sh

#!/bin/bash

# Claude Code notification script
# Usage: claude-notify.sh <type> [message] [details]

TYPE=$1
MESSAGE=${2:-"Claude Code notification"}
DETAILS=${3:-""}

# Check if Discord config exists
DISCORD_CONFIG="$HOME/.claude/discord-config.json"
USE_DISCORD=false
if [ -f "$DISCORD_CONFIG" ] && [ -f "$HOME/bin/claude-discord-notify.sh" ]; then
    # Check if webhook URL is configured (without jq)
    if grep -q '"webhook_url"' "$DISCORD_CONFIG" && grep -q 'discord.com/api/webhooks' "$DISCORD_CONFIG"; then
        USE_DISCORD=true
    fi
fi

# 通知方法の自動検出
notify() {
    local title=$1
    local message=$2
    local urgency=${3:-normal}
    local icon=${4:-dialog-information}
    
    # notify-sendが利用可能な場合
    if command -v notify-send &> /dev/null; then
        notify-send "$title" "$message" -i "$icon" -u "$urgency"
    # macOSの場合
    elif command -v osascript &> /dev/null; then
        osascript -e "display notification \"$message\" with title \"$title\""
    # WSL環境の場合(PowerShell経由)
    elif command -v powershell.exe &> /dev/null; then
        powershell.exe -Command "
            Add-Type -AssemblyName System.Windows.Forms
            \$notification = New-Object System.Windows.Forms.NotifyIcon
            \$notification.Icon = [System.Drawing.SystemIcons]::Information
            \$notification.BalloonTipIcon = 'Info'
            \$notification.BalloonTipTitle = '$title'
            \$notification.BalloonTipText = '$message'
            \$notification.Visible = \$true
            \$notification.ShowBalloonTip(5000)
            Start-Sleep -Seconds 5
            \$notification.Dispose()
        "
    else
        # フォールバック: ターミナルに表示
        echo "[$title] $message" >&2
    fi
}

# Discord通知を優先的に使用
if [ "$USE_DISCORD" = true ] && ([ "$TYPE" = "waiting" ] || [ "$TYPE" = "permission" ]); then
    # Discord通知スクリプトを呼び出し
    "$HOME/bin/claude-discord-notify.sh" "$TYPE" "$MESSAGE" "$DETAILS"
    exit $?
fi

# 通常の通知処理
case "$TYPE" in
    "complete")
        # 作業完了通知
        notify "Claude Code" "作業が完了しました: $MESSAGE" "normal" "dialog-information"
        
        # Discord通知も送信(応答待ち不要なので)
        if [ "$USE_DISCORD" = true ]; then
            "$HOME/bin/claude-discord-notify.sh" "$TYPE" "$MESSAGE" "$DETAILS" &
        fi
        ;;
    "waiting"|"permission")
        # 入力待ち通知(重要度高)
        notify "Claude Code" "あなたの入力を待っています: $MESSAGE" "critical" "dialog-warning"
        
        # 音声通知(利用可能な場合)
        if command -v paplay &> /dev/null; then
            # システムサウンドを再生
            if [ -f "/usr/share/sounds/freedesktop/stereo/message.oga" ]; then
                paplay /usr/share/sounds/freedesktop/stereo/message.oga 2>/dev/null
            fi
        elif command -v powershell.exe &> /dev/null; then
            # WSL環境でビープ音
            powershell.exe -Command "[console]::beep(1000,500)"
        else
            # 代替: ビープ音
            echo -e '\a'
        fi
        ;;
    *)
        # デフォルト通知
        notify "Claude Code" "$MESSAGE" "normal" "dialog-information"
        ;;
esac

bin/claude-discord-notify.sh

#!/bin/bash

# Claude Code Discord notification script with rich embeds
# Usage: claude-discord-notify.sh <type> [message] [details]

TYPE=$1
MESSAGE=${2:-"Claude Code notification"}
DETAILS=${3:-""}

# Configuration file path
CONFIG_FILE="$HOME/.claude/discord-config.json"

# Load Discord webhook URL from config
if [ -f "$CONFIG_FILE" ]; then
    WEBHOOK_URL=$(jq -r '.webhook_url' "$CONFIG_FILE" 2>/dev/null)
    BOT_TOKEN=$(jq -r '.bot_token' "$CONFIG_FILE" 2>/dev/null)
    CHANNEL_ID=$(jq -r '.channel_id' "$CONFIG_FILE" 2>/dev/null)
else
    echo "Error: Discord config file not found at $CONFIG_FILE" >&2
    exit 1
fi

if [ -z "$WEBHOOK_URL" ]; then
    echo "Error: Discord webhook URL not configured" >&2
    exit 1
fi

# Response file for bot communication
RESPONSE_FILE="/tmp/claude-discord-response-$$"
MESSAGE_ID_FILE="/tmp/claude-discord-message-id-$$"

# Colors for different notification types
case "$TYPE" in
    "complete")
        COLOR="5763719"  # Green
        TITLE="✅ 作業完了"
        EMOJI="✅"
        ;;
    "waiting")
        COLOR="16705372"  # Orange
        TITLE="⏳ 入力待機中"
        EMOJI="⏳"
        ;;
    "permission")
        COLOR="15105570"  # Red
        TITLE="🔐 権限要求"
        EMOJI="🔐"
        ;;
    "error")
        COLOR="15158332"  # Dark Red
        TITLE="❌ エラー"
        EMOJI="❌"
        ;;
    *)
        COLOR="3447003"  # Blue
        TITLE="ℹ️ 通知"
        EMOJI="ℹ️"
        ;;
esac

# Get current timestamp
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")

# Get additional context information
HOSTNAME=$(hostname)
USER=$(whoami)
PWD=$(pwd)

# Create rich embed JSON
create_embed() {
    local question_type=""
    local action_required=""
    
    # Determine question type and required action
    case "$TYPE" in
        "permission")
            question_type="コマンド実行の許可"
            action_required="✅ 承認 | ❌ 拒否"
            ;;
        "waiting")
            question_type="ユーザー入力待ち"
            action_required="💬 返信が必要"
            ;;
        *)
            question_type="情報"
            action_required="アクションは不要"
            ;;
    esac

    cat <<EOF
{
    "username": "Claude Code Assistant",
    "avatar_url": "https://www.anthropic.com/images/icons/apple-touch-icon.png",
    "content": "$EMOJI **Claude Code からの通知**",
    "embeds": [{
        "title": "$TITLE",
        "description": "$MESSAGE",
        "color": $COLOR,
        "fields": [
            {
                "name": "📁 作業ディレクトリ",
                "value": "\`$PWD\`",
                "inline": true
            },
            {
                "name": "👤 ユーザー",
                "value": "\`$USER@$HOSTNAME\`",
                "inline": true
            },
            {
                "name": "📝 詳細",
                "value": "${DETAILS:-なし}",
                "inline": false
            },
            {
                "name": "🎯 必要なアクション",
                "value": "$action_required",
                "inline": false
            }
        ],
        "footer": {
            "text": "Claude Code Hook System",
            "icon_url": "https://www.anthropic.com/favicon.ico"
        },
        "timestamp": "$TIMESTAMP"
    }]
}
EOF
}

# Send notification to Discord
send_notification() {
    local response=$(curl -s -X POST "$WEBHOOK_URL?wait=true" \
        -H "Content-Type: application/json" \
        -d "$(create_embed)")
    
    # Extract message ID from response
    local message_id=$(echo "$response" | jq -r '.id' 2>/dev/null)
    
    if [ -n "$message_id" ] && [ "$message_id" != "null" ]; then
        echo "$message_id" > "$MESSAGE_ID_FILE"
        echo "Discord notification sent. Message ID: $message_id" >&2
        
        # For permission/waiting types, add reaction options
        if [ "$TYPE" = "permission" ] || [ "$TYPE" = "waiting" ]; then
            # Store metadata for bot to process
            cat <<EOF > "${MESSAGE_ID_FILE}.meta"
{
    "message_id": "$message_id",
    "type": "$TYPE",
    "process_id": "$$",
    "response_file": "$RESPONSE_FILE",
    "timestamp": $(date +%s)
}
EOF
        fi
    else
        echo "Failed to send Discord notification" >&2
        return 1
    fi
}

# Wait for response (for interactive notifications)
wait_for_response() {
    if [ "$TYPE" = "permission" ] || [ "$TYPE" = "waiting" ]; then
        echo "Waiting for Discord response at $RESPONSE_FILE..." >&2
        
        # Wait for response file to be created by bot
        local timeout=300  # 5 minutes timeout
        local elapsed=0
        
        while [ ! -f "$RESPONSE_FILE" ] && [ $elapsed -lt $timeout ]; do
            sleep 1
            elapsed=$((elapsed + 1))
        done
        
        if [ -f "$RESPONSE_FILE" ]; then
            local response=$(cat "$RESPONSE_FILE")
            echo "Received response: $response" >&2
            
            # Clean up
            rm -f "$RESPONSE_FILE" "${MESSAGE_ID_FILE}" "${MESSAGE_ID_FILE}.meta"
            
            # Return appropriate exit code based on response
            case "$response" in
                "approved"|"yes"|"true")
                    exit 0
                    ;;
                "denied"|"no"|"false")
                    exit 1
                    ;;
                *)
                    exit 2
                    ;;
            esac
        else
            echo "Timeout waiting for response" >&2
            rm -f "${MESSAGE_ID_FILE}" "${MESSAGE_ID_FILE}.meta"
            exit 3
        fi
    fi
}

# Main execution
send_notification
wait_for_response

projects/claude-discord-bot/run.py

#!/usr/bin/env python3
"""Entry point for the Discord bot"""

import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))

from claude_discord_bot.main import main
import asyncio

if __name__ == "__main__":
    asyncio.run(main())

projects/claude-discord-bot/src/claude_discord_bot/main.py

#!/usr/bin/env python3
"""
Claude Code Discord Bot
Monitors reactions on Claude Code notifications and sends responses back
"""

import asyncio
import json
import os
import sys
import time
from pathlib import Path
from typing import Dict, Optional

import discord
from discord.ext import commands, tasks

# Load configuration
CONFIG_PATH = Path.home() / ".claude" / "discord-config.json"
TEMP_DIR = Path("/tmp")

class ClaudeDiscordBot(commands.Bot):
    def __init__(self):
        # Use minimal intents - we only need to track reactions
        intents = discord.Intents.default()
        intents.reactions = True
        intents.guilds = True
        
        super().__init__(command_prefix='!', intents=intents)
        
        self.config = self.load_config()
        self.pending_messages: Dict[str, dict] = {}
        
    def load_config(self) -> dict:
        """Load configuration from JSON file"""
        if not CONFIG_PATH.exists():
            print(f"Error: Config file not found at {CONFIG_PATH}")
            sys.exit(1)
            
        with open(CONFIG_PATH) as f:
            return json.load(f)
    
    async def on_ready(self):
        """Bot is ready and connected"""
        print(f'Bot connected as {self.user}')
        self.check_message_files.start()
        
    @tasks.loop(seconds=1)
    async def check_message_files(self):
        """Check for new message metadata files"""
        meta_files = list(TEMP_DIR.glob("claude-discord-message-id-*.meta"))
        if meta_files:
            print(f"Found {len(meta_files)} metadata files")
        
        for meta_file in meta_files:
            try:
                with open(meta_file) as f:
                    metadata = json.load(f)
                
                message_id = metadata['message_id']
                
                # Skip if already tracking
                if message_id in self.pending_messages:
                    continue
                
                # Check if message is not too old (5 minutes)
                if time.time() - metadata['timestamp'] > 300:
                    meta_file.unlink()
                    continue
                
                # Add to tracking
                self.pending_messages[message_id] = metadata
                
                # Add reactions to the message
                channel = self.get_channel(int(self.config['channel_id']))
                if channel:
                    try:
                        message = await channel.fetch_message(int(message_id))
                        
                        if metadata['type'] == 'permission':
                            await message.add_reaction(self.config['reactions']['approve'])
                            await message.add_reaction(self.config['reactions']['deny'])
                        elif metadata['type'] == 'waiting':
                            # For waiting type, also add approve/deny options
                            await message.add_reaction(self.config['reactions']['approve'])
                            await message.add_reaction(self.config['reactions']['deny'])
                            await message.add_reaction('💬')
                            
                    except discord.NotFound:
                        print(f"Message {message_id} not found")
                        del self.pending_messages[message_id]
                        meta_file.unlink()
                        
            except Exception as e:
                print(f"Error processing meta file {meta_file}: {e}")
    
    async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
        """Handle reaction additions"""
        # Ignore bot's own reactions
        if payload.user_id == self.user.id:
            return
        
        # Check if user is allowed
        if str(payload.user_id) not in self.config.get('allowed_users', []):
            return
        
        message_id = str(payload.message_id)
        
        # Check if this is a tracked message
        if message_id not in self.pending_messages:
            return
        
        metadata = self.pending_messages[message_id]
        response_file = Path(metadata['response_file'])
        
        # Determine response based on reaction
        emoji = str(payload.emoji)
        response = None
        
        if emoji == self.config['reactions']['approve']:
            response = "approved"
        elif emoji == self.config['reactions']['deny']:
            response = "denied"
        elif emoji == '💬':
            # For waiting type, just acknowledge
            response = "acknowledged"
        
        if response:
            print(f"Writing response '{response}' to {response_file}")
            # Write response to file
            try:
                response_file.write_text(response)
                print(f"Successfully wrote response to {response_file}")
            except Exception as e:
                print(f"Error writing response: {e}")
            
            # Clean up tracking
            del self.pending_messages[message_id]
            
            # Update message to show it was processed
            channel = self.get_channel(payload.channel_id)
            if channel:
                try:
                    message = await channel.fetch_message(payload.message_id)
                    
                    # Add a checkmark to show it was processed
                    await message.add_reaction('☑️')
                    
                    # Edit the message to show who responded
                    user = await self.fetch_user(payload.user_id)
                    
                    if message.embeds:
                        embed = message.embeds[0]
                        embed.add_field(
                            name="✅ 処理済み",
                            value=f"処理者: {user.mention}\n応答: {response}",
                            inline=False
                        )
                        await message.edit(embed=embed)
                        
                except discord.NotFound:
                    pass
    
    async def cleanup_old_messages(self):
        """Clean up old pending messages"""
        current_time = time.time()
        to_remove = []
        
        for message_id, metadata in self.pending_messages.items():
            if current_time - metadata['timestamp'] > self.config.get('reaction_timeout', 300):
                to_remove.append(message_id)
                
                # Write timeout response
                response_file = Path(metadata['response_file'])
                response_file.write_text("timeout")
        
        for message_id in to_remove:
            del self.pending_messages[message_id]

async def main():
    """Main entry point"""
    bot = ClaudeDiscordBot()
    
    try:
        await bot.start(bot.config['bot_token'])
    except KeyboardInterrupt:
        await bot.close()
    except Exception as e:
        print(f"Error: {e}")
        await bot.close()

if __name__ == "__main__":
    asyncio.run(main())

.claude/discord-config.example.json

{
    "webhook_url": "https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN",
    "bot_token": "YOUR_BOT_TOKEN",
    "channel_id": "YOUR_CHANNEL_ID",
    "allowed_users": ["YOUR_DISCORD_USER_ID"],
    "reaction_timeout": 300,
    "reactions": {
        "approve": "✅",
        "deny": "❌",
        "pause": "⏸️"
    }
}

これで作業完了・ユーザー許可を求めるメッセージがDiscordに流れるようにはなった。
が、リアクションをDiscord上でしても、Discord botがそれを検知こそすれど、claudeを実行している端末に応答が返ることは無い。

Pythonスクリプトだけいかにあげた。
https://github.com/SH11235/claude-discord-bot-py

まあ通知が来るだけでよしとは言えるが、Discordに通知を出すならDiscordでClaude Codeと相互作用をしたい。

そこで
https://github.com/KOBA789/human-in-the-loop
がよさそう

sh11235sh11235

会話のログをDiscordに流すtoolsを追加した
明示的にこの会話をDiscordに流して、と指定すればしばらくDiscordに作ったスレッドにメッセージを流してくれる
https://github.com/SH11235/human-in-the-loop/commit/2ef73bb2f084c5136a7a9e33c9debd0eef18915b

human-in-the-loopとは主目的とみる画面が違う気がするので、別レポジトリ(別のMCP)として作る方がいい気がしてきた
実装を参考にさせていただきながら、新規レポジトリにする

このスクラップは3ヶ月前にクローズされました