Skip to content

Instantly share code, notes, and snippets.

@masuidrive
Last active January 30, 2026 16:44
Show Gist options
  • Select an option

  • Save masuidrive/3bd621d7c64a408fd5a1835302c3cf61 to your computer and use it in GitHub Desktop.

Select an option

Save masuidrive/3bd621d7c64a408fd5a1835302c3cf61 to your computer and use it in GitHub Desktop.
claude コマンドで先に`/install-github-app`をインストーしたあと、この `https://gist.github.com/masuidrive/3bd621d7c64a408fd5a1835302c3cf61 を読み込んでセットアップしてください。`

Claude Code Bot - GitHub Actions Integration

Complete implementation guide for running Claude Code Bot in GitHub Actions with devcontainer.

Always respond in the language used by the user in their request or in previous conversations.

Target Audiences:

  • 👤 Human readers: Overview and explanation sections
  • 🤖 Claude Code (setup executor): Follow "Setup Execution" section
  • 🚀 Claude Bot (runtime): Configuration in system.md

For Claude Code: Setup Execution

When instructed to "read and execute this gist":

  1. Create all configuration files
  2. Verify CLAUDE_CODE_OAUTH_TOKEN secret (see verification steps below)
  3. Commit changes with descriptive message
  4. Push to repository
  5. Verify workflow file is valid

CLAUDE_CODE_OAUTH_TOKEN Secret Verification

Before completing setup, verify the secret exists using this priority:

  1. If gh command is available: Check secret existence via API

    gh api repos/OWNER/REPO/actions/secrets | jq -r '.secrets[].name' | grep -q CLAUDE_CODE_OAUTH_TOKEN
    • If found: Proceed with commit/push
    • If not found: Warn user and provide setup instructions
  2. If .github/workflows/claude.yml exists: Assume secret is already configured

    • The existing claude-code-action setup likely has the secret configured
    • Proceed with commit/push
  3. If neither condition is met: Warn user

    • Inform them to set up CLAUDE_CODE_OAUTH_TOKEN in repository settings
    • Provide setup instructions
    • Still proceed with commit/push (workflow will fail gracefully with clear error)

Overview

This setup enables Claude to autonomously handle Issues and Pull Requests by:

  • Triggering on /code or 🤖 in Issue/PR comments
  • Running in a devcontainer with full development environment
  • Committing and pushing changes directly to branches
  • Providing real-time progress updates via comments

File Structure

.github/
  workflows/
    claude-bot.yml          # GitHub Actions workflow
  claude/
    run-action.sh          # Main automation script
    system.md              # Claude's system prompt
.devcontainer/
  devcontainer.json        # Container configuration
  Dockerfile               # Container image
.claude/
  CLAUDE.md                # Project-specific test instructions

Important: Autonomous Execution Policy

Claude Bot operates autonomously and should complete tasks end-to-end without stopping for user confirmation, unless:

  • Explicit user instruction is required (ambiguous requirements, multiple valid approaches)
  • Critical decisions that could have significant impact
  • Security-sensitive operations

Default behavior: Execute the full task workflow from start to finish.

When implementing tasks:

  • Read the full request and understand all requirements
  • Plan the complete solution before starting
  • Execute all steps including testing and verification
  • Wait for test results and validate outputs
  • Fix issues if tests fail and retry until all tests pass
  • Only stop when the task is fully complete or requires user input

Workflow Configuration

.github/workflows/claude-bot.yml

name: Claude Bot

on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
  issues:
    types: [opened, assigned]
  pull_request_review:
    types: [submitted]

jobs:
  claude:
    # Trigger condition: /code または 🤖 を含むコメント・Issue・Pull Request
    if: |
      (github.event_name == 'issue_comment' && (contains(github.event.comment.body, '/code') || contains(github.event.comment.body, '🤖'))) ||
      (github.event_name == 'pull_request_review_comment' && (contains(github.event.comment.body, '/code') || contains(github.event.comment.body, '🤖'))) ||
      (github.event_name == 'pull_request_review' && (contains(github.event.review.body, '/code') || contains(github.event.review.body, '🤖'))) ||
      (github.event_name == 'issues' && (contains(github.event.issue.body, '/code') || contains(github.event.issue.body, '🤖') || contains(github.event.issue.title, '/code') || contains(github.event.issue.title, '🤖')))

    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
      issues: write
      packages: write
      actions: read

    steps:
      # Step1: 即座に 👀 リアクションを追加
      - name: Add eyes reaction to comment
        run: |
          # Based on event typeコメントIDとURLを設定
          if [ "${{ github.event_name }}" = "issue_comment" ] || [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then
            COMMENT_ID="${{ github.event.comment.id }}"
            REACTION_URL="https://api.github.com/repos/${{ github.repository }}/issues/comments/$COMMENT_ID/reactions"
          elif [ "${{ github.event_name }}" = "pull_request_review" ]; then
            COMMENT_ID="${{ github.event.review.id }}"
            REACTION_URL="https://api.github.com/repos/${{ github.repository }}/pulls/comments/$COMMENT_ID/reactions"
          elif [ "${{ github.event_name }}" = "issues" ]; then
            COMMENT_ID="${{ github.event.issue.number }}"
            REACTION_URL="https://api.github.com/repos/${{ github.repository }}/issues/$COMMENT_ID/reactions"
          fi

          if [ -n "$COMMENT_ID" ]; then
            echo "Adding 👀 reaction (event: ${{ github.event_name }}, id: $COMMENT_ID)"
            curl -X POST \
              -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
              -H "Accept: application/vnd.github.v3+json" \
              "$REACTION_URL" \
              -d '{"content":"eyes"}'
          fi

      # Step2: コードチェックアウト
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: ${{ secrets.GITHUB_TOKEN }}

      # Step3: GHCR にログイン(イメージキャッシュ用)
      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # Step4: devcontainer で実行
      - name: Run Claude Bot in devcontainer
        id: run_claude
        uses: devcontainers/[email protected]
        with:
          configFile: .devcontainer/devcontainer.json
          imageName: ghcr.io/${{ github.repository }}/devcontainer-ci
          push: always
          cacheFrom: |
            ghcr.io/${{ github.repository }}/devcontainer-ci:latest
          env: |
            CLAUDE_CODE_OAUTH_TOKEN=${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
            GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
            GITHUB_REPOSITORY=${{ github.repository }}
            GITHUB_RUN_ID=${{ github.run_id }}
            ISSUE_NUMBER=${{ github.event.issue.number || github.event.pull_request.number }}
            EVENT_TYPE=${{ github.event_name }}
            COMMENT_ID=${{ github.event.comment.id }}
            DEVCONTAINER_CONFIG_PATH=.devcontainer/devcontainer.json
            CLAUDE_TIMEOUT=1800
          runCmd: |
            # Run Claude Bot
            ./.github/claude/run-action.sh

      # Error handling: 👀 を削除して 😟 を追加
      - name: Handle error - Remove eyes and add confused reaction
        if: failure()
        run: |
          # Based on event typeコメントIDとURLを設定
          if [ "${{ github.event_name }}" = "issue_comment" ]; then
            COMMENT_ID="${{ github.event.comment.id }}"
            REACTIONS_URL="https://api.github.com/repos/${{ github.repository }}/issues/comments/$COMMENT_ID/reactions"
            DELETE_URL_PREFIX="https://api.github.com/repos/${{ github.repository }}/issues/comments/$COMMENT_ID/reactions"
          elif [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then
            COMMENT_ID="${{ github.event.comment.id }}"
            REACTIONS_URL="https://api.github.com/repos/${{ github.repository }}/pulls/comments/$COMMENT_ID/reactions"
            DELETE_URL_PREFIX="https://api.github.com/repos/${{ github.repository }}/pulls/comments/$COMMENT_ID/reactions"
          elif [ "${{ github.event_name }}" = "pull_request_review" ]; then
            COMMENT_ID="${{ github.event.review.id }}"
            REACTIONS_URL="https://api.github.com/repos/${{ github.repository }}/pulls/comments/$COMMENT_ID/reactions"
            DELETE_URL_PREFIX="https://api.github.com/repos/${{ github.repository }}/pulls/comments/$COMMENT_ID/reactions"
          elif [ "${{ github.event_name }}" = "issues" ]; then
            COMMENT_ID="${{ github.event.issue.number }}"
            REACTIONS_URL="https://api.github.com/repos/${{ github.repository }}/issues/$COMMENT_ID/reactions"
            DELETE_URL_PREFIX="https://api.github.com/repos/${{ github.repository }}/issues/$COMMENT_ID/reactions"
          fi

          if [ -n "$COMMENT_ID" ]; then
            echo "Handling error (event: ${{ github.event_name }}, id: $COMMENT_ID)"

            # Get existing reactions and 👀 を見つける
            REACTIONS=$(curl -s \
              -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
              -H "Accept: application/vnd.github.v3+json" \
              "$REACTIONS_URL")

            echo "Reactions response:"
            echo "$REACTIONS" | jq .

            # 👀 (eyes) リアクションのIDを抽出して削除
            echo "$REACTIONS" | jq -r '.[] | select(.content == "eyes") | .id' | while read REACTION_ID; do
              if [ -n "$REACTION_ID" ]; then
                echo "Removing eyes reaction $REACTION_ID from $DELETE_URL_PREFIX/$REACTION_ID"
                curl -X DELETE \
                  -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
                  -H "Accept: application/vnd.github.v3+json" \
                  "$DELETE_URL_PREFIX/$REACTION_ID"
              fi
            done

            # 😟 (confused) リアクションを追加
            echo "Adding 😟 reaction"
            curl -X POST \
              -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
              -H "Accept: application/vnd.github.v3+json" \
              "$REACTIONS_URL" \
              -d '{"content":"confused"}'
          fi

Automation Script

.github/claude/run-action.sh

#!/bin/bash
set -e

echo "🤖 Claude Bot starting..."

# Display current directory
echo "📁 Current directory: $(pwd)"
echo "📁 Contents:"
ls -la

# Gitリポジトリが現在のディレクトリにあるか確認
if [ ! -d ".git" ]; then
  echo "⚠️ .git directory not found in current directory"

  # Find working directory
  if [ -d "/workspaces/review-apps/.git" ]; then
    cd /workspaces/review-apps
    echo "✅ Changed to /workspaces/review-apps"
  elif [ -d "/workspace/.git" ]; then
    cd /workspace
    echo "✅ Changed to /workspace"
  else
    echo "❌ Cannot find git repository"
    exit 1
  fi
fi

echo "📁 Working directory: $(pwd)"

# Check environment variables
if [ -z "$ISSUE_NUMBER" ] || [ -z "$GITHUB_REPOSITORY" ]; then
  echo "❌ Required environment variables are missing"
  echo "ISSUE_NUMBER: $ISSUE_NUMBER"
  echo "GITHUB_REPOSITORY: $GITHUB_REPOSITORY"
  exit 1
fi

echo "📋 Issue/PR: #$ISSUE_NUMBER"
echo "📦 Repository: $GITHUB_REPOSITORY"
echo "🎯 Event type: $EVENT_TYPE"

# Git 設定
git config --global --add safe.directory /workspaces/review-apps
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

# Fetch latest state
git fetch origin

# Issue/PR情報の取得
echo "📝 Fetching Issue/PR data..."
if [[ "$EVENT_TYPE" == "pull_request"* ]]; then
  # PR の場合
  PR_DATA=$(gh pr view $ISSUE_NUMBER \
    --json title,body,comments,headRefName \
    --repo $GITHUB_REPOSITORY)

  ISSUE_TITLE=$(echo "$PR_DATA" | jq -r '.title')
  ISSUE_BODY=$(echo "$PR_DATA" | jq -r '.body // ""')
  COMMENTS=$(echo "$PR_DATA" | jq -r '.comments[]? | "[\(.author.login)] \(.body)"' | tail -10)

  # PR の場合: head ブランチ名を取得
  BRANCH_NAME=$(echo "$PR_DATA" | jq -r '.headRefName')
  echo "📌 PR head branch: $BRANCH_NAME"

  git checkout "$BRANCH_NAME"
  git pull origin "$BRANCH_NAME" || true

  # PR diff取得
  PR_DIFF=$(gh pr diff $ISSUE_NUMBER --repo $GITHUB_REPOSITORY | head -1000 || echo "")
else
  # Issue の場合
  ISSUE_DATA=$(gh issue view $ISSUE_NUMBER \
    --json title,body,comments \
    --repo $GITHUB_REPOSITORY)

  ISSUE_TITLE=$(echo "$ISSUE_DATA" | jq -r '.title')
  ISSUE_BODY=$(echo "$ISSUE_DATA" | jq -r '.body // ""')
  COMMENTS=$(echo "$ISSUE_DATA" | jq -r '.comments[]? | "[\(.author.login)] \(.body)"' | tail -10)

  # Issue の場合: 新しいブランチ名を作成
  BRANCH_NAME="claude-bot/issue-${ISSUE_NUMBER}"
  echo "📌 Issue branch: $BRANCH_NAME"

  if git ls-remote --heads origin "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then
    # Branch exists → checkout
    git checkout "$BRANCH_NAME"
    git pull origin "$BRANCH_NAME" || true
  else
    # Branch existsしない → 作成
    git checkout -b "$BRANCH_NAME"
  fi

  PR_DIFF=""
fi

# Extract latest user request(最後のコメント)
USER_REQUEST=$(echo "$COMMENTS" | tail -1 | sed -E 's/\/(code|🤖)//gi' || echo "$ISSUE_TITLE")

# main を merge
echo "🔀 Merging origin/main into $BRANCH_NAME..."
MERGE_OUTPUT=$(git merge origin/main --no-edit 2>&1) || MERGE_EXIT_CODE=$?
MERGE_EXIT_CODE=${MERGE_EXIT_CODE:-0}

CONFLICT_SECTION=""
if [ $MERGE_EXIT_CODE -ne 0 ]; then
  echo "⚠️ Merge conflict detected!"

  # conflict があれば、Claude に解決させる
  CONFLICT_FILES=$(git diff --name-only --diff-filter=U)

  CONFLICT_SECTION="

---

# 🚨 IMPORTANT: Git Merge Conflict Detected

**You MUST resolve the merge conflicts BEFORE starting the user's task.**

## Conflicted Files:
\`\`\`
$CONFLICT_FILES
\`\`\`

## Steps to Resolve:
1. Read each conflicted file
2. Understand both changes (current branch vs main)
3. Resolve conflicts by editing files (remove conflict markers <<<<<<, =======, >>>>>>>)
4. Stage resolved files: \`git add <file>\`
5. Commit the merge: \`git commit -m \"Merge main into $BRANCH_NAME\"\`
6. Verify: \`git status\` should show no conflicts

**After resolving conflicts, proceed with the user's original request.**
"
else
  echo "✅ Merge successful (no conflicts)"
fi

# Extract and download image URLs
echo "🖼️ Checking for attached images..."
IMAGE_DIR="/tmp/issue-${ISSUE_NUMBER}-images"
mkdir -p "$IMAGE_DIR"

# Extract image URLs from original Markdown(表示用)
ORIGINAL_IMAGE_URLS=$(echo "$ISSUE_BODY" | \
  grep -oE '(https?://[^)"\s]+\.(png|jpg|jpeg|gif|webp|svg))|(https?://github\.com/user-attachments/assets/[^)"\s]+)|(https?://user-images\.githubusercontent\.com/[^)"\s]+)' | \
  sort -u)

# GraphQL API を使って bodyHTML を取得(JWT付きの実際の画像URLを含む)
if [[ "$EVENT_TYPE" == "pull_request"* ]]; then
  BODY_HTML=$(gh api graphql -f query="
    query {
      repository(owner: \"$(echo $GITHUB_REPOSITORY | cut -d/ -f1)\", name: \"$(echo $GITHUB_REPOSITORY | cut -d/ -f2)\") {
        pullRequest(number: $ISSUE_NUMBER) {
          bodyHTML
        }
      }
    }
  " --jq '.data.repository.pullRequest.bodyHTML' 2>/dev/null || echo "")
else
  BODY_HTML=$(gh api graphql -f query="
    query {
      repository(owner: \"$(echo $GITHUB_REPOSITORY | cut -d/ -f1)\", name: \"$(echo $GITHUB_REPOSITORY | cut -d/ -f2)\") {
        issue(number: $ISSUE_NUMBER) {
          bodyHTML
        }
      }
    }
  " --jq '.data.repository.issue.bodyHTML' 2>/dev/null || echo "")
fi

# bodyHTML から画像URLを抽出(JWT付きのprivate-user-images URLと通常の画像URL)
# sed を使って href と src 属性から URL を抽出
DOWNLOAD_IMAGE_URLS=$(echo "$BODY_HTML" | \
  sed -n 's/.*\(href\|src\)="\([^"]*\)".*/\2/p' | \
  grep -E 'https?://(private-user-images\.githubusercontent\.com/[^[:space:]]+|[^[:space:]]+\.(png|jpg|jpeg|gif|webp|svg)(\?[^[:space:]]*)?|user-images\.githubusercontent\.com/[^[:space:]]+)' | \
  sort -u)

# Convert original and download URLs to arrays
IFS=$'\n' read -d '' -r -a ORIGINAL_URLS_ARRAY <<< "$ORIGINAL_IMAGE_URLS" || true
IFS=$'\n' read -d '' -r -a DOWNLOAD_URLS_ARRAY <<< "$DOWNLOAD_IMAGE_URLS" || true

IMAGE_COUNT=0
IMAGE_LIST=""
for i in "${!DOWNLOAD_URLS_ARRAY[@]}"; do
  download_url="${DOWNLOAD_URLS_ARRAY[$i]}"
  original_url="${ORIGINAL_URLS_ARRAY[$i]:-$download_url}"  # Use download URL if original URL is not available

  if [ -n "$download_url" ]; then
    IMAGE_COUNT=$((IMAGE_COUNT + 1))
    # Extract file extension(URLパラメータの前の部分から)
    EXT=$(echo "$download_url" | sed -E 's/^.*\.([a-z]+)(\?.*)?$/\1/' | grep -E '^(png|jpg|jpeg|gif|webp|svg)$' || echo "png")
    FILENAME="image-${IMAGE_COUNT}.${EXT}"
    IMAGE_PATH="$IMAGE_DIR/$FILENAME"

    echo "  - Downloading: ${download_url:0:80}..."
    if curl -sL "$download_url" -o "$IMAGE_PATH" 2>/dev/null && [ -s "$IMAGE_PATH" ]; then
      # Verify file was downloaded correctly
      FILE_TYPE=$(file -b "$IMAGE_PATH" 2>/dev/null)
      if echo "$FILE_TYPE" | grep -qE "image|RIFF.*Web/P"; then
        IMAGE_LIST="$IMAGE_LIST
- $IMAGE_PATH (source: $original_url)"
        echo "    ✓ Saved to: $IMAGE_PATH ($FILE_TYPE)"
      else
        echo "    ✗ Not a valid image file: $FILE_TYPE"
        rm -f "$IMAGE_PATH"
        IMAGE_COUNT=$((IMAGE_COUNT - 1))
      fi
    else
      echo "    ✗ Failed to download"
      IMAGE_COUNT=$((IMAGE_COUNT - 1))
    fi
  fi
done

if [ $IMAGE_COUNT -gt 0 ]; then
  echo "✅ Downloaded $IMAGE_COUNT image(s)"
elif [ -n "$DOWNLOAD_IMAGE_URLS" ]; then
  echo "ℹ️ No images found in Issue/PR"
fi

# Build images section
IMAGES_SECTION=""
if [ $IMAGE_COUNT -gt 0 ]; then
  IMAGES_SECTION="

---

# 📸 Attached Images

**IMPORTANT**: The user has attached $IMAGE_COUNT image(s) to this Issue/Pull Request.

## Image Files:
$IMAGE_LIST

## Instructions:
1. **Read each image** using the Read tool to understand the visual content
2. **Analyze the images** in the context of the user's request
3. **Reference the images** in your response when relevant

Use these images to better understand the user's requirements, bugs, design requests, or other visual information.
"
fi

# Load system prompt
SYSTEM_PROMPT=$(cat .github/claude/system.md | \
  sed "s|{DEVCONTAINER_CONFIG_PATH}|$DEVCONTAINER_CONFIG_PATH|g")

# Build full prompt
FULL_PROMPT="$SYSTEM_PROMPT

---

# Issue/PR Context

**Type**: $EVENT_TYPE
**Number**: #$ISSUE_NUMBER
**Title**: $ISSUE_TITLE

## Description
$ISSUE_BODY

## Recent Comments
$COMMENTS

## Latest Request
$USER_REQUEST"

if [ -n "$PR_DIFF" ]; then
  FULL_PROMPT="$FULL_PROMPT

## PR Diff (first 1000 lines)
\`\`\`
$PR_DIFF
\`\`\`"
fi

FULL_PROMPT="$FULL_PROMPT
$CONFLICT_SECTION
$IMAGES_SECTION

---

# Your Working Branch

**Branch**: \`$BRANCH_NAME\`
**GitHub Comparison**: https://github.com/$GITHUB_REPOSITORY/compare/main...$BRANCH_NAME

You are working on this branch. All commits will be pushed here.
Users can view your changes by visiting the comparison page.

---

# Environment Variables Available
- ISSUE_NUMBER: $ISSUE_NUMBER
- GITHUB_REPOSITORY: $GITHUB_REPOSITORY
- BRANCH_NAME: $BRANCH_NAME
"

# Save prompt to file
echo "$FULL_PROMPT" > "/tmp/claude-prompt-$ISSUE_NUMBER.txt"

# Post initial comment
echo "💬 Posting initial progress comment..."
PROGRESS_COMMENT_ID=$(gh api repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/comments \
  -f body="🤖 **Working...**" --jq '.id')

echo "Progress comment ID: $PROGRESS_COMMENT_ID"

# Claude CLI authentication setup for CI environment
echo "🔑 Setting up Claude CLI authentication..."
if [ -n "$CLAUDE_CODE_OAUTH_TOKEN" ]; then
  # CLAUDE_CODE_OAUTH_TOKEN はそのまま Claude CLI が認識する
  echo "✅ CLAUDE_CODE_OAUTH_TOKEN is set (long-lived token)"
else
  echo "❌ ERROR: CLAUDE_CODE_OAUTH_TOKEN is not set!"
  echo "Please set this secret in GitHub repository settings."
  exit 1
fi

# Claude CLI をバックグラウンドで実行(JSON streaming)
JSON_OUTPUT_FILE="/tmp/claude-output-$ISSUE_NUMBER.json"
PROGRESS_OUTPUT_FILE="/tmp/claude-progress-$ISSUE_NUMBER.txt"  # for progress(thinking + text)
RESULT_OUTPUT_FILE="/tmp/claude-result-$ISSUE_NUMBER.txt"      # for final result(textのみ)
TASK_STATUS_FILE="/tmp/claude-tasks-$ISSUE_NUMBER.txt"         # task status(常に最新)
TIMEOUT_VALUE=${CLAUDE_TIMEOUT:-1800}

echo "🚀 Starting Claude Code CLI (timeout: ${TIMEOUT_VALUE}s)..."

# =============================================================================
# Claude stream-json parsing
# =============================================================================
# Purpose: Claude CLIの出力から最終結果のみを抽出
#
# Problem: Claudeは作業中に複数のtext blockを出力する
#   1. "画像を確認します" (途中のつぶやき)
#   2. [tool_use: Read実行]
#   3. "画像を分析しました" (さらなるつぶやき)
#   4. [tool_use: 複数のツール実行]
#   5. 実際の分析結果 ← これだけを最終結果として表示したい
#
# Solution: content blockをindex別に管理し、message_stopで最後のtext blockのみ抽出
#
# Stream-JSON event構造 (Claude API仕様):
#   content_block_start (index: N, type: "text"|"tool_use")
#     content_block_delta (delta.type: "text_delta"|"input_json_delta")
#     content_block_delta ...
#   content_block_stop (index: N)
#   ...
#   message_stop ← 全メッセージ完了のシグナル
#
# Note: Claude API仕様は将来変更される可能性があります
# =============================================================================

(
  > "$PROGRESS_OUTPUT_FILE"  # Initialize progress file
  > "$TASK_STATUS_FILE"       # Initialize task status file

  CURRENT_TOOL=""
  CURRENT_TOOL_INPUT=""
  CURRENT_BLOCK_INDEX=""
  CURRENT_BLOCK_TYPE=""
  MESSAGE_COUNTER=0  # Track which message/turn we're on to avoid block file overwrites
  BLOCKS_DIR="/tmp/claude-blocks-$ISSUE_NUMBER"
  mkdir -p "$BLOCKS_DIR"

  timeout $TIMEOUT_VALUE claude -p --dangerously-skip-permissions \
    --output-format stream-json --include-partial-messages --verbose \
    < "/tmp/claude-prompt-$ISSUE_NUMBER.txt" 2>&1 | \
  while IFS= read -r line; do
    echo "$line" >> "$JSON_OUTPUT_FILE"

    # -------------------------------------------------------------------------
    # content_block_start: 新しいcontent block(text または tool_use)の開始を検出
    # -------------------------------------------------------------------------
    # Purpose: 各blockをindex別に管理し、typeを記録する
    # JSON: {"type":"stream_event","event":{"type":"content_block_start","index":N,"content_block":{"type":"text"|"tool_use"}}}
    BLOCK_START=$(echo "$line" | jq -r 'select(.type=="stream_event" and .event.type=="content_block_start") | .event' 2>/dev/null)
    if [ -n "$BLOCK_START" ] && [ "$BLOCK_START" != "null" ]; then
      CURRENT_BLOCK_INDEX=$(echo "$BLOCK_START" | jq -r '.index')
      CURRENT_BLOCK_TYPE=$(echo "$BLOCK_START" | jq -r '.content_block.type')

      echo "DEBUG: content_block_start - index=$CURRENT_BLOCK_INDEX, type=$CURRENT_BLOCK_TYPE" >&2

      # Save each block to individual file(メッセージ番号を含めて上書きを防ぐ)
      > "$BLOCKS_DIR/block-m$MESSAGE_COUNTER-$CURRENT_BLOCK_INDEX.txt"
      echo "$CURRENT_BLOCK_TYPE" > "$BLOCKS_DIR/block-m$MESSAGE_COUNTER-$CURRENT_BLOCK_INDEX.type"

      # For tool_use blocks, extract tool name
      if [ "$CURRENT_BLOCK_TYPE" = "tool_use" ]; then
        TOOL_NAME=$(echo "$BLOCK_START" | jq -r '.content_block.name')
        CURRENT_TOOL="$TOOL_NAME"
        CURRENT_TOOL_INPUT=""
      fi
    fi

    # Accumulate tool input JSON
    if [ -n "$CURRENT_TOOL" ]; then
      INPUT_DELTA=$(echo "$line" | jq -r 'select(.type=="stream_event" and .event.type=="content_block_delta" and .event.delta.type=="input_json_delta") | .event.delta.partial_json' 2>/dev/null)
      if [ -n "$INPUT_DELTA" ] && [ "$INPUT_DELTA" != "null" ]; then
        CURRENT_TOOL_INPUT="${CURRENT_TOOL_INPUT}${INPUT_DELTA}"
      fi
    fi

    # Detect content_block_stop
    BLOCK_STOP_INDEX=$(echo "$line" | jq -r 'select(.type=="stream_event" and .event.type=="content_block_stop") | .event.index' 2>/dev/null)
    if [ -n "$BLOCK_STOP_INDEX" ] && [ "$BLOCK_STOP_INDEX" != "null" ]; then
      # Handle tools
      if [ -n "$CURRENT_TOOL" ]; then
        echo "DEBUG: Tool completed: $CURRENT_TOOL" >&2

        # Display tool execution details in progress
        case "$CURRENT_TOOL" in
          Bash)
            DESCRIPTION=$(echo "$CURRENT_TOOL_INPUT" | jq -r '.description // empty' 2>/dev/null)
            if [ -n "$DESCRIPTION" ]; then
              printf "🔧 [Bash: %s]\n" "$DESCRIPTION" >> "$PROGRESS_OUTPUT_FILE"
            else
              printf "🔧 [Bash実行中...]\n" >> "$PROGRESS_OUTPUT_FILE"
            fi
            ;;
          Read)
            FILE_PATH=$(echo "$CURRENT_TOOL_INPUT" | jq -r '.file_path // empty' 2>/dev/null)
            if [ -n "$FILE_PATH" ]; then
              printf "🔧 [Read: %s]\n" "$FILE_PATH" >> "$PROGRESS_OUTPUT_FILE"
            else
              printf "🔧 [Read実行中...]\n" >> "$PROGRESS_OUTPUT_FILE"
            fi
            ;;
          Write)
            FILE_PATH=$(echo "$CURRENT_TOOL_INPUT" | jq -r '.file_path // empty' 2>/dev/null)
            if [ -n "$FILE_PATH" ]; then
              printf "🔧 [Write: %s]\n" "$FILE_PATH" >> "$PROGRESS_OUTPUT_FILE"
            else
              printf "🔧 [Write実行中...]\n" >> "$PROGRESS_OUTPUT_FILE"
            fi
            ;;
          Edit|Grep|Glob)
            printf "🔧 [%s実行中...]\n" "$CURRENT_TOOL" >> "$PROGRESS_OUTPUT_FILE"
            ;;
          TodoWrite|TaskCreate|TaskUpdate)
            echo "DEBUG: Processing task tool: $CURRENT_TOOL" >&2
            TASKS=$(echo "$CURRENT_TOOL_INPUT" | jq -r '.todos[]? // .subject? // empty' 2>/dev/null)
            if [ -n "$TASKS" ]; then
              echo "DEBUG: Found tasks, updating TASK_STATUS_FILE" >&2
              > "$TASK_STATUS_FILE"
              echo "$CURRENT_TOOL_INPUT" | jq -r '.todos[]? | "  \(if .status == "completed" then "✅" elif .status == "in_progress" then "🔄" else "◻️" end) \(.content // .subject)"' 2>/dev/null > "$TASK_STATUS_FILE" || true
              echo "DEBUG: TASK_STATUS_FILE content:" >&2
              cat "$TASK_STATUS_FILE" >&2
            else
              echo "DEBUG: No tasks found in tool input" >&2
            fi
            ;;
        esac
        CURRENT_TOOL=""
        CURRENT_TOOL_INPUT=""
      fi

      CURRENT_BLOCK_INDEX=""
      CURRENT_BLOCK_TYPE=""
    fi

    # Extract thinking_delta (progress only)
    THINKING=$(echo "$line" | jq -r 'select(.type=="stream_event" and .event.type=="content_block_delta" and .event.delta.type=="thinking_delta") | .event.delta.thinking' 2>/dev/null)
    if [ -n "$THINKING" ] && [ "$THINKING" != "null" ]; then
      printf "%s" "$THINKING" >> "$PROGRESS_OUTPUT_FILE"
    fi

    # -------------------------------------------------------------------------
    # text_delta: テキスト出力の増分
    # -------------------------------------------------------------------------
    # Purpose: 進捗ファイルには全て保存、各blockファイルには該当indexのみ保存
    # JSON: {"type":"stream_event","event":{"type":"content_block_delta","index":N,"delta":{"type":"text_delta","text":"..."}}}
    TEXT_DELTA=$(echo "$line" | jq -r 'select(.type=="stream_event" and .event.type=="content_block_delta" and .event.delta.type=="text_delta") | .event' 2>/dev/null)
    if [ -n "$TEXT_DELTA" ] && [ "$TEXT_DELTA" != "null" ]; then
      TEXT=$(echo "$TEXT_DELTA" | jq -r '.delta.text')
      BLOCK_IDX=$(echo "$TEXT_DELTA" | jq -r '.index')

      # All text to progress file(途中のつぶやきも含む)を保存
      printf "%s" "$TEXT" >> "$PROGRESS_OUTPUT_FILE"

      # Save to each block file by index(後で最後のblockのみ抽出)
      if [ -n "$BLOCK_IDX" ] && [ "$BLOCK_IDX" != "null" ]; then
        printf "%s" "$TEXT" >> "$BLOCKS_DIR/block-m$MESSAGE_COUNTER-$BLOCK_IDX.txt"
      fi
    fi

    # -------------------------------------------------------------------------
    # message_stop: メッセージターン完了シグナル
    # -------------------------------------------------------------------------
    # Purpose: MESSAGE_COUNTERをインクリメントして次のターンのblock ID衝突を防ぐ
    # JSON: {"type":"stream_event","event":{"type":"message_stop"}}
    MESSAGE_STOP=$(echo "$line" | jq -r 'select(.type=="stream_event" and .event.type=="message_stop") | .type' 2>/dev/null)
    if [ -n "$MESSAGE_STOP" ] && [ "$MESSAGE_STOP" != "null" ]; then
      MESSAGE_COUNTER=$((MESSAGE_COUNTER + 1))
      echo "DEBUG: message_stop detected, incremented MESSAGE_COUNTER to $MESSAGE_COUNTER" >&2
    fi
  done
) &
CLAUDE_PID=$!

echo "Claude PID: $CLAUDE_PID"

# GitHub Actions URL を取得
ACTIONS_URL="https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"

# Update progress periodically(10秒ごと)
UPDATE_COUNT=0
while kill -0 $CLAUDE_PID 2>/dev/null; do
  sleep 10
  UPDATE_COUNT=$((UPDATE_COUNT + 1))

  # Get last part of output(最大2000文字)
  CURRENT_OUTPUT=$(tail -c 2000 "$PROGRESS_OUTPUT_FILE" 2>/dev/null || echo "(Waiting for output...)")

  # task statusを取得
  TASK_STATUS=""
  if [ -s "$TASK_STATUS_FILE" ]; then
    TASK_STATUS=$(cat "$TASK_STATUS_FILE")
  fi

  # GitHub Actionsログに進捗の最後20行を出力
  echo "========== Claude Progress (last 20 lines) =========="
  tail -20 "$PROGRESS_OUTPUT_FILE" 2>/dev/null || echo "(Waiting for output...)"
  echo "====================================================="

  # Update comment(タスク状態はcode blockの外)
  echo "📝 Updating progress comment (update $UPDATE_COUNT)..."

  # Build comment body
  COMMENT_BODY="🤖 **Working...** (更新 $UPDATE_COUNT)"

  # Add task status if exists (outside code block)
  if [ -n "$TASK_STATUS" ]; then
    COMMENT_BODY="${COMMENT_BODY}

${TASK_STATUS}"
  fi

  # Add output in code block
  COMMENT_BODY="${COMMENT_BODY}

~~~
${CURRENT_OUTPUT}
~~~

🔗 [View job details]($ACTIONS_URL)"

  gh api -X PATCH repos/$GITHUB_REPOSITORY/issues/comments/$PROGRESS_COMMENT_ID \
    -f body="$COMMENT_BODY" || echo "Warning: Failed to update comment"
done

# Post final result after completion
wait $CLAUDE_PID
CLAUDE_EXIT_CODE=$?

echo "Claude finished with exit code: $CLAUDE_EXIT_CODE"

# Extract final result
# Priority: /tmp/ccbot-result.md > 最後のtext block
CCBOT_RESULT_FILE="/tmp/ccbot-result.md"

if [ -f "$CCBOT_RESULT_FILE" ]; then
  echo "✅ Found /tmp/ccbot-result.md - using it as final result"
  cat "$CCBOT_RESULT_FILE" > "$RESULT_OUTPUT_FILE"
else
  echo "⚠️  /tmp/ccbot-result.md not found - falling back to last text block extraction"

  # Extract last text block from all turns
  # Multiple turns(メッセージのやり取り)が発生するため、全block-m*-*.txtから最新を探す
  BLOCKS_DIR="/tmp/claude-blocks-$ISSUE_NUMBER"
  if [ -d "$BLOCKS_DIR" ]; then
    echo "Extracting last text block from all message turns..."
    LAST_TEXT_BLOCK=""
    LAST_MESSAGE_NUM=-1
    LAST_INDEX=-1

    # block-mN-I.txt 形式のファイルを全て走査(N=メッセージ番号, I=ブロックインデックス)
    for block_file in "$BLOCKS_DIR"/block-m*-*.txt; do
      if [ -f "$block_file" ]; then
        # Extract message number and index from filename: block-m2-0.txt -> MSG=2, IDX=0
        BASENAME=$(basename "$block_file" .txt)
        MSG_NUM=$(echo "$BASENAME" | sed 's/block-m\([0-9]*\)-.*/\1/')
        BLOCK_IDX=$(echo "$BASENAME" | sed 's/block-m[0-9]*-\([0-9]*\)/\1/')
        TYPE_FILE="$BLOCKS_DIR/block-m$MSG_NUM-$BLOCK_IDX.type"

        if [ -f "$TYPE_FILE" ]; then
          BLOCK_TYPE=$(cat "$TYPE_FILE")
          echo "  Found block-m$MSG_NUM-$BLOCK_IDX: type=$BLOCK_TYPE"

          if [ "$BLOCK_TYPE" = "text" ]; then
            # Update if newer message or larger index in same message
            if [ "$MSG_NUM" -gt "$LAST_MESSAGE_NUM" ] || \
               ([ "$MSG_NUM" -eq "$LAST_MESSAGE_NUM" ] && [ "$BLOCK_IDX" -gt "$LAST_INDEX" ]); then
              LAST_TEXT_BLOCK="$block_file"
              LAST_MESSAGE_NUM=$MSG_NUM
              LAST_INDEX=$BLOCK_IDX
              echo "    -> Updated LAST_TEXT_BLOCK to block-m$MSG_NUM-$BLOCK_IDX"
            fi
          fi
        fi
      fi
    done

    if [ -n "$LAST_TEXT_BLOCK" ] && [ -f "$LAST_TEXT_BLOCK" ]; then
      echo "Writing last text block ($(basename "$LAST_TEXT_BLOCK")) to RESULT_OUTPUT_FILE"
      cat "$LAST_TEXT_BLOCK" > "$RESULT_OUTPUT_FILE"
    else
      echo "WARNING: No text blocks found!"
    fi
  fi
fi

# Post final result(text outputのみ、thinkingは除外)
CLAUDE_OUTPUT=$(cat "$RESULT_OUTPUT_FILE")

if [ $CLAUDE_EXIT_CODE -eq 0 ]; then
  echo "✅ Task completed successfully"

  # Success: 👀 リアクションを削除してから 👍 を追加
  REACTIONS_URL=""
  DELETE_URL_PREFIX=""

  if [ -n "$COMMENT_ID" ]; then
    # For reply to comment: コメントのリアクションを操作
    if [ "$EVENT_TYPE" = "issue_comment" ]; then
      REACTIONS_URL="repos/$GITHUB_REPOSITORY/issues/comments/$COMMENT_ID/reactions"
      DELETE_URL_PREFIX="repos/$GITHUB_REPOSITORY/issues/comments/$COMMENT_ID/reactions"
    elif [ "$EVENT_TYPE" = "pull_request_review_comment" ]; then
      REACTIONS_URL="repos/$GITHUB_REPOSITORY/pulls/comments/$COMMENT_ID/reactions"
      DELETE_URL_PREFIX="repos/$GITHUB_REPOSITORY/pulls/comments/$COMMENT_ID/reactions"
    elif [ "$EVENT_TYPE" = "pull_request_review" ]; then
      REACTIONS_URL="repos/$GITHUB_REPOSITORY/pulls/comments/$COMMENT_ID/reactions"
      DELETE_URL_PREFIX="repos/$GITHUB_REPOSITORY/pulls/comments/$COMMENT_ID/reactions"
    fi
  else
    # New Issue/PR の場合: Issue/PR 自体のリアクションを操作
    if [[ "$EVENT_TYPE" == "issues" ]]; then
      REACTIONS_URL="repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/reactions"
      DELETE_URL_PREFIX="repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/reactions"
    elif [[ "$EVENT_TYPE" == "pull_request"* ]]; then
      REACTIONS_URL="repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/reactions"
      DELETE_URL_PREFIX="repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/reactions"
    fi
  fi

  if [ -n "$REACTIONS_URL" ]; then
    # 👀 リアクションを削除
    echo "Removing 👀 reaction from $REACTIONS_URL..."
    REACTIONS=$(gh api "$REACTIONS_URL" 2>/dev/null || echo "[]")
    echo "$REACTIONS" | jq -r '.[] | select(.content == "eyes") | .id' | while read REACTION_ID; do
      if [ -n "$REACTION_ID" ]; then
        echo "  Deleting reaction ID: $REACTION_ID"
        gh api -X DELETE "$DELETE_URL_PREFIX/$REACTION_ID" 2>/dev/null || echo "  Warning: Failed to delete reaction"
      fi
    done

    # 👍 リアクションを追加
    echo "Adding 👍 reaction..."
    gh api -X POST "$REACTIONS_URL" \
      -f content="+1" || echo "Warning: Failed to add reaction"
  fi

  # Post final result(ブランチ情報付き)
  gh api -X PATCH repos/$GITHUB_REPOSITORY/issues/comments/$PROGRESS_COMMENT_ID \
    -f body="$CLAUDE_OUTPUT

---

🌿 Branch: \`$BRANCH_NAME\`
📝 [View changes](https://github.com/$GITHUB_REPOSITORY/compare/main...$BRANCH_NAME)"
else
  echo "❌ Task failed with exit code $CLAUDE_EXIT_CODE"

  # Failed: エラー情報を投稿
  gh api -X PATCH repos/$GITHUB_REPOSITORY/issues/comments/$PROGRESS_COMMENT_ID \
    -f body="## ❌ Task Failed

\`\`\`
$CLAUDE_OUTPUT
\`\`\`

Exit code: $CLAUDE_EXIT_CODE"

  exit $CLAUDE_EXIT_CODE
fi

echo "🎉 Claude Bot finished!"

System Prompt

.github/claude/system.md

# Execution Workflow (Read First)

**Your work consists of TWO phases: DO THE WORK → REPORT THE RESULT**

Every task follows this pattern:
1. **Do the work** (read files, create tasks, write code, run tests, etc.)
2. **Report the result** (output final deliverable as text)

**The report is NOT optional. You MUST always end with a text output containing the result.**

---

## Standard Workflow

### Phase 1: Understand & Plan

1. **Check blocking conditions**
   - If merge conflicts exist → resolve first
   - If instructions violate constraints → stop and adjust

2. **Classify the request**
   - Document/Analysis: Creates plans, reports, investigations
   - Code/Implementation: Modifies code, config, tests

3. **Create tasks** (optional for documents, recommended for code)
   - Use `TaskCreate` to break down work
   - Update with `TaskUpdate` as you progress

### Phase 2: Execute Work

4. **Read context**
   - Read Issue/PR title, description, comments
   - Review attached images if present
   - Use Read/Glob/Grep to explore codebase

5. **Do the work**
   - For documents: Research and gather information
   - For code: Make changes, run tests, commit & push
   - Follow existing style and architecture

6. **Test and verify (MANDATORY)**
   - **Always run tests** after any implementation or configuration changes
   - Wait for test results and analyze output
   - If tests pass → proceed to next step
   - If tests fail → analyze failures, fix issues, and retry
   - Repeat until all tests pass or reach a blocking issue
   - **Never skip test validation under any circumstances**
   - This applies to: setup, configuration, code changes, updates, installations, etc.

7. **Decide persistence**
   - Project Files → commit to repository
   - Auxiliary Artifacts → upload as GitHub Assets
   - (See Artifact Handling Policy below)

### Phase 3: Report Result (CRITICAL)

8. **🚨 Write final report to `/tmp/ccbot-result.md` 🚨**
   - **This step is MANDATORY**
   - Use Write tool to create `/tmp/ccbot-result.md`
   - Content: The deliverable (plan, analysis, implementation summary)
   - Must be <3000 characters, self-contained, with necessary information
   - See "Final Report Format" section below

**Important:**
- Do NOT just output text - WRITE TO THE FILE
- The file `/tmp/ccbot-result.md` will be automatically posted as the final comment
- If you skip this step, users will see incomplete intermediate text like "I'm working on it..."

---

# Definitions (Critical)

## Project Files

Files that are **part of the codebase or permanent project state**.

Includes:
* Source code (any language)
* Configuration files (including YAML, CI workflows)
* Tests
* Documentation intended to live in the repository (`docs/`, `README`, etc.)

**Rule**:
If a file affects project behavior or is part of the codebase, it is a Project File.

---

## Auxiliary Artifacts

Files generated **only to support Issue / PR discussion or review**.

Includes:
* Screenshots
* Logs, traces, debug output
* Temporary diagrams or visualizations
* Temporary data exports (CSV, JSON, etc.)
* Test result outputs

---

## Generated Files (Clarification)

In this document, **"generated files" refers ONLY to Auxiliary Artifacts**.
It does **NOT** include Project Files.

---

## Rule Precedence (Highest First)

1. **Project Files policy**
2. **Output self-contained requirement**
3. **Auxiliary Artifacts policy**
4. All other rules

---

# Role

You are an **autonomous development assistant** running on **GitHub Actions**.

* No human-in-the-loop
* You independently decide actions
* You are responsible for correctness, persistence, and reporting

---

# Execution Environment

* Running inside a **devcontainer**
* Configuration path: `{DEVCONTAINER_CONFIG_PATH}`
* This is a **CI environment**

### Critical properties
* Filesystem is **ephemeral**
* Users **cannot access local files**
* **Git is the only persistence mechanism**

### Available Tools

**GitHub CLI (`gh`):**
* GitHub API operations
* Issue/PR management
* Assets upload (primary method for Auxiliary Artifacts)

**Git:**
* Version control
* Only persistence mechanism for Project Files

**Development tools:**
* Project-specific (language runtimes, test frameworks, etc.)

---

# Output Language

**Language Selection Priority (highest first):**

1. **User's explicit request** (highest priority)
   - If user says "in English" or "日本語で" → use that language

2. **Issue/PR language** (primary)
   - Detect the **primary language** from Issue/PR title and body
   - **Judgment criteria**: Which language is used for the main sentence structure?

   **How to detect primary language:**
   - Check what language the sentence **starts with**
   - Check what language makes up the **majority** of the text
   - Ignore technical terms and proper nouns mixed in

   **Examples:****English primary:**
   - "Create Express.js app" → English (pure English)
   - "Create Express.js app with 認証機能" → English (starts with "Create", main structure is English)
   - "Implement 日本語サポート feature" → English (starts with "Implement")

   ✅ **Japanese primary:**
   - "TODOアプリの実装計画を作成" → Japanese (pure Japanese)
   - "Express.jsアプリを作成 with authentication" → Japanese (starts with "Express.jsアプリを", main structure is Japanese)
   - "認証機能を実装して" → Japanese (starts with Japanese verb)

   ❌ **Common mistakes to avoid:**
   - "Create app with 認証" → Do NOT respond in Japanese just because it contains 認証
   - The primary language is English (starts with "Create")

3. **Default: English** (fallback)
   - Use English if no clear language detected
   - **When multiple language options are possible**, use the AskUserQuestion tool to clarify the preferred language

**What to write in the detected language:**
* All status updates and explanations
* Final report content
* Error messages
* Test result summaries
* Commit messages (except `Co-Authored-By`)

**Complete Examples:**

| Issue Title/Body | Primary Language | Response Language |
|------------------|------------------|-------------------|
| "Create TODO app implementation plan" | English | English |
| "Create Express.js app with 認証機能とタスク管理" | English (starts with "Create") | English |
| "TODOアプリの実装計画を作成して" | Japanese | Japanese |
| "Express.jsでアプリを作成 with auth" | Japanese (starts with "Express.jsで") | Japanese |
| "Please respond in French" | English + explicit French request | French (explicit wins) |

---

# Persistence & Constraints

* Local files are deleted after workflow completion
* Writing files locally does NOT make them visible to users
* Any uncommitted work is permanently lost
* For Project Files, you MUST commit and push before finishing

---

# Artifact Handling Policy

## Project Files (Repository)

You MUST commit Project Files to the repository.

Includes:
* Code
* YAML / config files
* Tests
* Permanent documentation

User permission is **NOT required**.

---

## Auxiliary Artifacts (GitHub Assets)

You MUST use GitHub Assets by default.

Includes:
* Screenshots
* Logs, traces, debug output
* Temporary diagrams or flowcharts
* Test results
* Any non-permanent generated files

### How to Upload to GitHub Assets

```bash
# Basic pattern
ASSET_URL=$(gh api repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/assets \
  -F [email protected] \
  --jq '.browser_download_url')

# Embed in comment
echo "![Description]($ASSET_URL)"
```

**Examples:**

**Screenshot:**
```bash
ASSET_URL=$(gh api repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/assets \
  -F [email protected] \
  --jq '.browser_download_url')
echo "![Screenshot]($ASSET_URL)"
```

**Log file:**
```bash
ASSET_URL=$(gh api repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/assets \
  -F [email protected] \
  --jq '.browser_download_url')
echo "Debug log: [debug.log]($ASSET_URL)"
```

**JSON/CSV data:**
```bash
ASSET_URL=$(gh api repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/assets \
  -F [email protected] \
  --jq '.browser_download_url')
echo "Results: [results.json]($ASSET_URL)"
```

### Rationale: Why Issue/PR-only Files MUST Use Assets

Auxiliary Artifacts exist solely to support Issue or PR discussion.

They are:
* Temporary
* Review-oriented
* Not part of the project's permanent state

Because of this:
* They MUST NOT be committed to the repository
* They MUST be uploaded as GitHub Issue / PR Assets

This keeps repository history clean and scopes review materials correctly.

**When in doubt, ALWAYS choose Assets.**

---

## Repository Exception for Images

Images may be committed **ONLY IF**:
* The user explicitly requests adding them to `docs/` or permanent documentation

---

## Small vs Large Text Artifacts

### Small text (<100 lines)
* MUST be shown inline in the comment
* Use `~~~~~~~~~` fences to avoid delimiter conflicts

### Large text (logs, traces, dumps)
* MUST be uploaded as GitHub Assets

---

## Small Text Artifacts — Concrete Examples

### Correct
```
Retrieved results:

~~~json
{
  "status": "ok",
  "items": 3
}
~~~
```

### Incorrect
```
Saved results to result.json.
```

**Principle**:
If the user cannot see the content, it does not exist.

---

## Forbidden Phrases

You MUST NOT say the following unless the content is fully visible:
* "Saved to file"
* "Created X"
* "Generated Y"
* "Output written to …"

---

# Communication Model

Your final output is automatically posted as a GitHub comment. Users interact with you through these comments.

**User's viewing environment:**
* Users read your output in GitHub Issue/PR comments (web, mobile, or tablet)
* Clicking links to view files requires navigating to different pages - this is inconvenient
* Users want to understand your work by READING YOUR COMMENT, not by browsing files
* 🚨 **CRITICAL**: Include the actual content/results in your text output, not just file links
* Think of your output as a self-contained report that users can fully understand without clicking any links

**What this means:**
* Users primarily read comments, not files
* Links are supplementary only
* Output MUST be self-contained
* Actual content MUST be included in the comment

---

# Output Requirements (Hard Rules)

## Critical: Always End with Final Text Output

**Your last message MUST contain the final result as text.**

* After using tools (TaskCreate, Write, Bash, etc.), you MUST output the final result as text
* DO NOT end with tool use only - always follow with text output
* The text output should contain the deliverable content (plan, analysis, implementation summary, etc.)
* This applies regardless of whether you created tasks or not

**Example workflow:**
1. Use tools to perform work (TaskCreate, Write, etc.)
2. **Then output final result as text** ← This is mandatory
3. Users see your final text output as the GitHub comment

---

## Content Requirements

* The GitHub comment MUST be self-contained
* The comment MUST be **under 3000 characters**
* **必要な情報を網羅** - include all necessary information (no omissions)
* **読みやすく簡潔に** - concise and readable, avoid verbosity
* Code MUST be **minimal**
  * Function signatures
  * Key lines
  * Important snippets only
* Full implementations in comments are FORBIDDEN

### When Content Exceeds 3000 Characters

If content exceeds 3000 characters, reconsider your approach:
* Simplify explanations
* Remove redundant details
* Use more concise expressions

Only if truly necessary:
1. Create a detailed document file:
   * Project File (permanent): commit to repository
   * Auxiliary Artifact (temporary): upload as Asset
2. Comment MUST include:
   * Summary (<3000 characters)
   * Link (supplementary)

**Note:** Exceeding 3000 characters should be rare for typical tasks.

---

## Output Prohibitions

You MUST NOT:
* Output branch names
* Output GitHub compare or diff links
* End with vague statements only
* Post links without explaining results

**Rationale:**
* Branch names and diff links are automatically added by post-processing scripts
* Including them manually causes duplication
* Improves maintainability

**What to include instead:**
* Work description
* Result summary
* Test results
* Specific file links (individual files)

---

## Output Examples

### ❌ Wrong: Ending with tool use only

```
# User request: "Create implementation plan for TODO app"

# Your output:
[TaskCreate tool call]
[Write tool call to create docs/plan.md]
# ❌ Ends here - no final text output!
```

Result: User sees incomplete intermediate text like "要件を整理しました。次に実装計画を作成します。"

### ✅ Correct: Always end with final text

```
# User request: "Create implementation plan for TODO app"

# Your output:
[TaskCreate tool call]
[Write tool call to create docs/plan.md]

# Then output final result as text:
## Express.js TODO App Implementation Plan

### 1. Project Setup
- Express.js + TypeScript
- PostgreSQL + Prisma ORM
- JWT authentication

### 2. Data Models
- **User**: id, email, password_hash
- **Task**: id, user_id, title, status, priority

### 3. API Endpoints
- POST /api/auth/register
- POST /api/auth/login
- GET /api/tasks
- POST /api/tasks
- PUT /api/tasks/:id
- DELETE /api/tasks/:id

### 4. Implementation Phases
1. Project setup + database
2. Authentication system
3. Task CRUD operations
4. Testing + documentation

Complete implementation plan created.
```

Result: User sees the full plan in the comment.

---

# Final Report Format

After completing your work, you MUST write the final report to `/tmp/ccbot-result.md`.

**How to write the report:**
```bash
cat > /tmp/ccbot-result.md <<'EOF'
[Your report content here]
EOF
```

## Report Template

### For Documents (Plans, Analysis, Investigation)

```markdown
## [Title of Deliverable]

### [Section 1]
[Key points, findings, or design details]

### [Section 2]
[More details]

### [Section 3]
[Implementation steps, recommendations, or conclusions]

[Summary statement]
```

**Requirements:**
- Under 3000 characters total
- Include all necessary information
- Code snippets: minimal (function signatures only)
- Self-contained (user doesn't need to click links)

### For Code Implementation

```markdown
## [What was implemented]

### Changes Made
- [file1.ts](link) - Brief description
- [file2.ts](link) - Brief description

### Key Functions
```typescript
// Function signatures only
async function authenticate(user, password): Promise<Token>
```

### Test ResultsAll tests passed (X/X)
- Feature A tests
- Feature B tests

### Summary
[What was accomplished and current state]
```

**Requirements:**
- Under 3000 characters total
- Test results mandatory
- Link to changed files
- Brief code excerpts only (no full implementations)

## Complete Example Workflow**Wrong: Ending without writing result file**
```
[TaskCreate - create tasks]
[Write tool - create docs/plan.md]
[Bash - commit and push]
# ❌ Ends here - no /tmp/ccbot-result.md written!
```
Result: User sees "Now I'll create..." instead of the actual plan.

---

✅ **Correct: Always write to /tmp/ccbot-result.md**

```bash
# Step 1: Do the work
[TaskCreate - create tasks]
[Write tool - create docs/plan.md with full implementation plan]
[Bash - commit and push]

# Step 2: Write final report to /tmp/ccbot-result.md
cat > /tmp/ccbot-result.md <<'EOF'
## Express.js TODO App Implementation Plan

### 1. Project Setup
- Express.js + TypeScript
- PostgreSQL + Prisma ORM
- JWT authentication

### 2. Data Models
- **User**: id, email, password_hash
- **Task**: id, user_id, title, status, priority

### 3. API Endpoints
- POST /api/auth/register - User registration
- POST /api/auth/login - Login
- GET /api/tasks - List tasks
- POST /api/tasks - Create task
- PUT /api/tasks/:id - Update task
- DELETE /api/tasks/:id - Delete task

### 4. Implementation Phases
1. Project setup + database
2. Authentication system
3. Task CRUD operations
4. Testing + documentation

### 5. Detailed Documentation
📄 Complete plan: [docs/todo-implementation-plan.md](link)

Implementation plan created and committed.
EOF
```

Result: User sees the complete implementation plan in the comment.

---

# Git Workflow

* Correct branch is already checked out
* `main` has already been merged
* Merge conflicts MUST be resolved first

## Mandatory Git Command Sequences

### Resolve Merge Conflicts
```bash
git add <resolved-files>
git commit -m "Merge main into current branch"
```

### Standard Flow

```bash
git add .
git commit -m "<type>: <summary>

<optional body>

Co-Authored-By: Claude Bot <[email protected]>"
```

```bash
CURRENT_BRANCH=$(git branch --show-current)
git push origin "$CURRENT_BRANCH"
```

**Finishing without pushing is strictly forbidden.**

---

# Referencing Repository Files

## Important: Only Reference Files You Modified

**DO NOT include links to files you didn't modify in your final report.**

Common mistake:
```markdown
❌ The fix in [run-action.sh](https://github.com/repo/blob/claude-bot/issue-22/.github/claude/run-action.sh)
```
This creates a 404 error because `.github/claude/run-action.sh` wasn't modified in this branch.

**Rules:**
* Only link to files you created or modified
* Configuration files (`.github/`, `.devcontainer/`, etc.) should NOT be linked unless you modified them
* If you need to reference existing files, describe them in text without links

## Code / Text Files You Modified

```
https://github.com/$GITHUB_REPOSITORY/blob/$BRANCH_NAME/path/to/file
```

**Example (files you created/modified):**
```markdown[src/auth/controller.ts](https://github.com/$GITHUB_REPOSITORY/blob/$BRANCH_NAME/src/auth/controller.ts)[docs/implementation-plan.md](https://github.com/$GITHUB_REPOSITORY/blob/$BRANCH_NAME/docs/implementation-plan.md)
```

## Images

* PNG / JPG / GIF → `?raw=true`
* SVG → `?sanitize=true`

---

# Error Handling & Recovery

## Test Failures
* Fix ALL test failures before committing
* If unable to fix, report in Issue/PR comment with details

## API / Service Failures
* GitHub API failure: retry 3 times, then report error
* External dependency failure: consider alternatives

## Unresolvable Issues
* Clearly report error details
* List attempted solutions
* Ask user for guidance

---

# Security Policy

## Never Commit These Files
* `.env`, `.env.local` - environment variables
* `credentials.json`, `secrets.yaml` - credentials
* `*.pem`, `*.key`, `*.p12` - private keys
* `config/database.yml` (with passwords)

## When Discovered
1. Remove from staging: `git reset HEAD <file>`
2. Add to `.gitignore`
3. Warn user

## Code Vulnerabilities
* Watch for SQL injection, XSS, CSRF
* Fix vulnerabilities before committing

---

# Rollback & Recovery

## Before Commit
```bash
git reset --hard HEAD  # Discard all changes
git checkout -- <file>  # Restore specific file
```

## After Commit (Before Push)
```bash
git reset --soft HEAD~1  # Undo commit, keep changes
git reset --hard HEAD~1  # Undo commit and changes
```

## After Push
* Use `git revert` to create revert commit
* **NEVER** use `--force` push

## When to Rollback
* All tests failing
* Build completely broken
* User explicitly requests

---

# Execution Time Constraints

* **Maximum execution time**: GitHub Actions timeout (typically 30-60 minutes)
* **Long-running tasks**:
  * Report progress at 10-minute mark
  * Consider splitting if exceeding 30 minutes

## Avoiding Infinite Loops
* If same error repeats 3 times, stop and report
* Run tests only once per implementation (re-run after fixes)

---

# Governing Principle

**If the user reads only your comment and nothing else,
they must fully understand what you did and what the result is.**


---

# Governing Principle

**If the user reads only your comment and nothing else,
they must fully understand what you did and what the result is.**

Devcontainer Configuration

.devcontainer/devcontainer.json

{
  "name": "Development Container",
  "build": {
    "dockerfile": "Dockerfile"
  },
  "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
  "remoteUser": "node",
  "features": {
    "ghcr.io/devcontainers/features/git:1": {},
    "ghcr.io/devcontainers/features/github-cli:1": {},
    "ghcr.io/devcontainers/features/common-utils:2": {
      "installZsh": false,
      "installOhMyZsh": false,
      "upgradePackages": false
    }
  },
  "customizations": {
    "vscode": {
      "extensions": []
    }
  }
}

Container Image

.devcontainer/Dockerfile

# Use official devcontainer base image with TypeScript and Node.js pre-configured
# This image is optimized for VS Code devcontainer development
FROM mcr.microsoft.com/devcontainers/typescript-node:22-bookworm

# Switch to node user (default non-root user in this devcontainer image)
USER node

# Install Claude Code CLI as node user using curl
RUN curl -fsSL https://claude.ai/install.sh | bash

# Add ~/.local/bin to PATH for Claude CLI
ENV PATH="/home/node/.local/bin:${PATH}"

WORKDIR /workspaces

Adapting to Existing Devcontainer

If you already have a .devcontainer/ setup, you need to add the following.

Required Commands

The following commands must be available in your devcontainer:

  • git - Version control
  • gh (GitHub CLI) - GitHub API operations
  • jq - JSON processing
  • curl - Downloads and API calls
  • file - File type detection
  • timeout - Command timeout control (usually included in coreutils)

If any of these are missing, add them via Dockerfile or devcontainer features.

Installing Claude Code CLI

Add the following to your existing Dockerfile or devcontainer configuration:

For Dockerfile:

# After switching to non-root user (e.g., USER node, USER vscode)
USER your_user_name

# Install Claude Code CLI
RUN curl -fsSL https://claude.ai/install.sh | bash

# Add to PATH
ENV PATH="/home/your_user_name/.local/bin:${PATH}"

For devcontainer.json:

{
  "postCreateCommand": "curl -fsSL https://claude.ai/install.sh | bash && echo 'export PATH=\"$HOME/.local/bin:$PATH\"' >> ~/.bashrc"
}

Important Notes

  • Install Claude CLI as the same user specified in remoteUser
  • Always add ~/.local/bin to PATH
  • .github/workflows/claude-bot.yml and .github/claude/ directory are required
  • Set CLAUDE_CODE_OAUTH_TOKEN as a GitHub repository secret

Reference: For a complete configuration example for new setups, see the "Devcontainer Configuration" section below.

Project Test Configuration

.claude/CLAUDE.md

## How to Run Tests

```bash
# Run all tests
# Depending on the project, use one of the following:
# npm test
# pytest
# bundle exec rake test
# go test ./...
# mvn test
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment