iPhone から GitHub Issue を書くだけで Mac の Claude Code が自動実行される仕組みを作った
TL;DR
GitHub Self-hosted Runner + Claude Code CLI を組み合わせて、iPhone で Issue を作成 → Mac 上の Claude Code が自動で調査・コード修正・PR 作成まで行う仕組みを構築した。2ステージ承認フロー付きで、/claude で調査、/approve で実行という安全な運用が可能。
モチベーション
普段 Mac の前にいないときでも、iPhone からコードの調査や修正を指示したい。GitHub Issue に書くだけで Claude Code が自動的に動いてくれれば、移動中でもコードレビューや修正依頼ができる。
完成した仕組み
iPhone GitHub Mac (self-hosted runner)
│ │ │
├─ Issue 作成 ────────────→│ │
│ (claude ラベル付与) │── ワークフロー起動 ─────→│
│ │ ├─ claude -p (読み取り専用)
│ │←── 調査結果コメント ──────┤
│ │ + 承認待ちラベル │
│ │ │
├─ /approve コメント ──────→│ │
│ │── ワークフロー起動 ─────→│
│ │ ├─ claude -p (書き込み許可)
│ │←── 実行結果 + PR ────────┤
│ │ - 承認待ちラベル │
トリガー一覧
| 操作 | 条件 | 動作 |
|---|---|---|
claude ラベル付き Issue 起票 | 起票者がリポジトリオーナー | Stage 1: 読み取り専用で調査 |
/claude コメント | コメント者がリポジトリオーナー | Stage 1: 読み取り専用で調査 |
/claude --execute コメント | コメント者がリポジトリオーナー | Stage 1 スキップ、直接実行 |
/approve コメント | コメント者がリポジトリオーナー + 承認待ち ラベル | Stage 2: 計画に基づき実行 |
必要なもの
- macOS マシン (Apple Silicon / Intel)
- Claude Code CLI がインストール済み (
npm install -g @anthropic-ai/claude-code) - Claude Max/Pro プラン (OAuth トークン) または Anthropic API キー
- GitHub CLI (
gh)
セットアップ手順
1. Self-hosted Runner のインストール
GitHub の個人アカウントでは Runner はリポジトリ単位の登録になる。同一マシンに複数の Runner を並置できる。
# ディレクトリ作成
mkdir -p ~/actions-runner/my-repo
cd ~/actions-runner/my-repo
# Runner パッケージのダウンロード (GitHub Settings > Actions > Runners で最新 URL を確認)
curl -o actions-runner-osx-arm64.tar.gz -L \
https://github.com/actions/runner/releases/download/v2.331.0/actions-runner-osx-arm64-2.331.0.tar.gz
tar xzf actions-runner-osx-arm64.tar.gz
# 登録トークンの取得
TOKEN=$(gh api repos/OWNER/REPO/actions/runners/registration-token -X POST --jq '.token')
# Runner の設定 (名前は任意)
./config.sh --url https://github.com/OWNER/REPO --token $TOKEN --name mac-runner --unattended
# macOS の LaunchAgent として常駐化 (ログイン時に自動起動)
./svc.sh install
./svc.sh start
2つ目以降のリポジトリは、tarball をコピーして同様に設定すればよい:
mkdir -p ~/actions-runner/another-repo
cd ~/actions-runner/another-repo
cp ~/actions-runner/my-repo/actions-runner-osx-arm64.tar.gz .
tar xzf actions-runner-osx-arm64.tar.gz
# 以降同じ手順
2. Claude Code の認証トークンを登録
Max/Pro プランの場合 (推奨):
# ブラウザ認証でOAuth トークンを生成 (有効期限: 1年)
claude setup-token
# 表示されたトークンを GitHub Secret に登録
gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo OWNER/REPO
API キーの場合:
gh secret set ANTHROPIC_API_KEY --repo OWNER/REPO
注意: Max プランで
ANTHROPIC_API_KEYを設定しようとすると、シェルに環境変数がないため空で登録されてしまう。Max/Pro プランでは必ずclaude setup-tokenを使うこと。
3. ラベルの作成
gh label create claude --color 7C3AED --description "Claude Code 自動処理" --repo OWNER/REPO
gh label create 承認待ち --color FCD34D --description "Claude Code 実行承認待ち" --repo OWNER/REPO
4. ワークフローファイルの配置
.github/workflows/claude-issue.yml をリポジトリに追加する。
claude-issue.yml (全文)
name: Claude Code on Issue
on:
issues:
types: [labeled]
issue_comment:
types: [created]
jobs:
claude-work:
# リポジトリオーナーのみ実行可能
if: >-
(
github.event_name == 'issues' &&
contains(github.event.issue.labels.*.name, 'claude') &&
github.event.issue.user.login == github.repository_owner
) || (
github.event_name == 'issue_comment' &&
github.event.comment.user.login == github.repository_owner &&
(startsWith(github.event.comment.body, '/claude') || startsWith(github.event.comment.body, '/approve'))
)
runs-on: self-hosted
permissions:
contents: write
issues: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Detect user home
id: env
run: |
echo "home=$(eval echo ~$(whoami))" >> "$GITHUB_OUTPUT"
- name: Determine mode
id: mode
uses: actions/github-script@v7
with:
script: |
const event = context.eventName;
let mode = 'investigate';
if (event === 'issue_comment') {
const body = context.payload.comment.body.trim();
if (body.startsWith('/approve')) {
const labels = context.payload.issue.labels.map(l => l.name);
if (!labels.includes('承認待ち')) {
core.setFailed('承認待ちラベルがありません。先に /claude で調査を実行してください。');
return;
}
mode = 'execute';
} else if (body.startsWith('/claude')) {
const args = body.replace('/claude', '').trim();
if (args === '--execute') {
mode = 'execute';
} else {
mode = 'investigate';
}
}
} else {
mode = 'investigate';
}
core.setOutput('mode', mode);
console.log(`Mode: ${mode}`);
- name: Build prompt
id: prompt
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
const mode = '${{ steps.mode.outputs.mode }}';
let prompt = `GitHub Issue #${issue.number}: ${issue.title}\n\n${issue.body}`;
if (context.eventName === 'issue_comment') {
const comment = context.payload.comment.body.trim();
let instruction = '';
if (comment.startsWith('/approve')) {
instruction = comment.replace('/approve', '').trim();
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
});
const planComment = comments.data
.reverse()
.find(c => c.body.includes('Claude Code 調査結果'));
if (planComment) {
prompt += `\n\n## 承認済み計画\n\n${planComment.body}`;
}
} else {
instruction = comment.replace('/claude', '').replace('--execute', '').trim();
}
if (instruction) {
prompt += `\n\n追加指示: ${instruction}`;
}
}
if (mode === 'investigate') {
prompt += '\n\n## 指示\n';
prompt += '読み取り専用モードです。コードの調査と分析のみ行ってください。\n';
prompt += '以下を出力してください:\n';
prompt += '1. 問題の分析結果\n';
prompt += '2. 変更が必要なファイルの一覧\n';
prompt += '3. 実行予定の変更内容\n';
prompt += '4. 作成予定の PR の概要\n';
} else {
prompt += '\n\n## 指示\n';
prompt += '書き込み許可モードです。計画に基づきファイル変更・コミット・PR 作成を実行してください。\n';
prompt += '作業が完了したら結果のサマリを出力してください。\n';
}
const fs = require('fs');
fs.writeFileSync('/tmp/claude-prompt.txt', prompt);
- name: Run Claude Code
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
HOME: ${{ steps.env.outputs.home }}
PATH: ${{ steps.env.outputs.home }}/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin
run: |
MODE="${{ steps.mode.outputs.mode }}"
if [ "$MODE" = "investigate" ]; then
TOOLS="Read,Glob,Grep"
else
TOOLS="Bash,Read,Write,Edit,Glob,Grep"
fi
claude -p "$(cat /tmp/claude-prompt.txt)" \
--allowedTools "$TOOLS" \
--output-format json > /tmp/claude-result.json 2>/tmp/claude-stderr.log || {
echo "Claude Code failed. Stderr:"
cat /tmp/claude-stderr.log 2>/dev/null
echo "Stdout:"
cat /tmp/claude-result.json 2>/dev/null || echo "(no output file)"
exit 1
}
- name: Post result to issue
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const mode = '${{ steps.mode.outputs.mode }}';
const raw = fs.readFileSync('/tmp/claude-result.json', 'utf8');
const result = JSON.parse(raw);
const body = result.result || result.text || JSON.stringify(result, null, 2);
const header = mode === 'investigate'
? '## Claude Code 調査結果'
: '## Claude Code 実行結果';
const footer = '---\n🤖 自動実行 by Claude Code (self-hosted runner)';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
body: `${header}\n\n${body}\n\n${footer}`
});
if (mode === 'investigate') {
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
labels: ['承認待ち']
});
} catch (e) {
console.log(`ラベル付与エラー: ${e.message}`);
}
}
if (mode === 'execute') {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
name: '承認待ち'
});
} catch (e) {
console.log(`ラベル除去エラー: ${e.message}`);
}
}
ハマったポイントと解決策
実際に構築する中で遭遇した問題を全て記録する。これが一番参考になるはず。
1. claude コマンドが見つからない
症状: Runner のジョブで claude: command not found
原因: Self-hosted Runner は macOS の LaunchAgent として動作するが、launchd サービスはユーザーの .zshrc / .bashrc を読まない。そのため ~/.local/bin が PATH に含まれない。
解決: ワークフローの env で明示的に PATH を指定する。
env:
HOME: ${{ steps.env.outputs.home }}
PATH: ${{ steps.env.outputs.home }}/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin
HOME も同様に上書きが必要。GitHub Actions Runner は作業ディレクトリを HOME に設定するため、Claude Code が設定ファイルを見つけられなくなる。
2. Issue 起票で2回実行される (二重発火)
症状: 1つの Issue を作成しただけなのに、ワークフローが2回走る
原因: on.issues.types: [opened, labeled] と設定していたため、claude ラベル付きで Issue を起票すると opened と labeled の両方のイベントが発火する。
解決: トリガーを [labeled] のみに変更。
on:
issues:
types: [labeled] # opened を含めない
ラベルなしで起票 → 後からラベルを付ける運用でも、ラベル付きで直接起票する運用でも、labeled イベント1回だけが発火する。
3. Max プランで認証エラー (Not logged in)
症状: "Not logged in · Please run /login" エラーで Claude Code が動かない
原因: Claude Max/Pro プランでは ANTHROPIC_API_KEY 環境変数は使わない。OAuth ログインで認証しているため、シェルに API キーが存在しない。gh secret set ANTHROPIC_API_KEY を実行しても空文字が登録される。
解決: claude setup-token で CI/CD 用の長期 OAuth トークンを生成する。
claude setup-token
# ブラウザが開く → 認証 → トークンが表示される
# このトークンを GitHub Secret CLAUDE_CODE_OAUTH_TOKEN に登録
ワークフロー側は ANTHROPIC_API_KEY ではなく CLAUDE_CODE_OAUTH_TOKEN を使う:
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
setup-token のトークンは1年間有効。claude login の対話的セッションとは異なる。
4. JSON パースエラー (stderr 混入)
症状: Post result to issue ステップで SyntaxError: Unexpected token ⚠ in JSON
原因: claude -p が stderr に警告メッセージ (⚠️ [BashTool]...) を出力する。2>&1 でリダイレクトしていたため、JSON 出力に警告文字列が混入し JSON.parse() が失敗。
解決: stderr を別ファイルにリダイレクトする。
# NG: stderr が stdout に混入
claude -p "..." --output-format json > /tmp/result.json 2>&1
# OK: stderr を別ファイルに分離
claude -p "..." --output-format json > /tmp/result.json 2>/tmp/stderr.log
失敗時のデバッグのために stderr も保存しておくと便利。
5. runner.home が存在しない
症状: PATH が /.local/bin:/opt/homebrew/bin:... になり、先頭にホームディレクトリがない
原因: GitHub Actions のコンテキストに runner.home は存在しない。${{ runner.home }} と書くと空文字に展開される。
解決: 別ステップで whoami を使ってホームディレクトリを取得する。
- name: Detect user home
id: env
run: |
echo "home=$(eval echo ~$(whoami))" >> "$GITHUB_OUTPUT"
- name: Run Claude Code
env:
HOME: ${{ steps.env.outputs.home }}
PATH: ${{ steps.env.outputs.home }}/.local/bin:...
6. gh secret get でシークレットを読めない
症状: 1つのリポジトリに設定した Secret を他のリポジトリにコピーしようとして失敗
原因: GitHub の仕様上、一度登録した Secret の値は読み取れない。gh secret get コマンドは存在しない。
解決: 各リポジトリで個別に gh secret set を実行する。同じトークンを繰り返し貼り付ける必要がある。
# 全リポジトリに同じトークンを登録
for repo in repo1 repo2 repo3; do
gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo "OWNER/$repo"
done
# → 毎回トークンの入力を求められる
運用ノート
Runner の起動・停止
Runner は macOS の LaunchAgent (~/Library/LaunchAgents/actions.runner.*.plist) として動作する。
cd ~/actions-runner/my-repo
./svc.sh status # 状態確認
./svc.sh stop # 一時停止 (再起動で自動復帰)
./svc.sh start # 起動
./svc.sh uninstall # 完全除去 (再起動しても起動しない)
| 操作 | Mac 再起動後 |
|---|---|
stop | 自動で再起動する (plist が残っているため) |
uninstall | 起動しない |
Mac 再起動時はログインが必要 (LaunchAgent はユーザーセッション開始後に起動)。自動ログインを有効にしておくと安心。
Runner が停止中に Issue を作ったら?
ワークフローのジョブは queued 状態で最大6時間待機する。Runner が復帰すればキューのジョブが実行される。6時間を超えるとキャンセル。
OAuth トークンの有効期限
| 認証方式 | 有効期限 |
|---|---|
claude setup-token | 1年間 |
claude login (対話的) | 8〜12時間 |
ANTHROPIC_API_KEY | 無期限 (従量課金) |
月次ヘルスチェック
トークンの期限切れを事前に検知するため、月1回のヘルスチェックワークフローを追加するのがおすすめ:
claude-health-check.yml
name: Claude Code Health Check
on:
schedule:
- cron: '0 9 1 * *' # 毎月1日 9:00 UTC
jobs:
check:
runs-on: self-hosted
steps:
- name: Detect user home
id: env
run: echo "home=$(eval echo ~$(whoami))" >> "$GITHUB_OUTPUT"
- name: Check Claude Code auth
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
HOME: ${{ steps.env.outputs.home }}
PATH: ${{ steps.env.outputs.home }}/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin
run: |
result=$(claude -p "Reply with only: OK" --output-format json 2>/dev/null)
if echo "$result" | grep -q '"is_error":true'; then
echo "::error::Claude Code 認証エラー — CLAUDE_CODE_OAUTH_TOKEN の更新が必要です"
echo "更新手順: claude setup-token を実行し、gh secret set CLAUDE_CODE_OAUTH_TOKEN で登録"
exit 1
fi
echo "Claude Code 認証OK"
失敗するとGitHub からメール通知が届く。最小プロンプト ("Reply with only: OK") なのでコストはほぼゼロ。
トークン更新手順
# 1. 新しいトークンを生成 (ブラウザ認証)
claude setup-token
# 2. 全リポジトリの Secret を更新
for repo in repo1 repo2 repo3; do
gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo "OWNER/$repo"
done
セキュリティ
実行者の制限
github.repository_owner との比較により、リポジトリオーナーのみがワークフローを起動できる。第三者が claude ラベルを付けたり /claude コメントしてもジョブは実行されない。
ツール制限
| ステージ | 許可ツール | リスク |
|---|---|---|
| Stage 1 (調査) | Read, Glob, Grep | ファイル読み取りのみ。変更不可 |
| Stage 2 (実行) | Bash, Read, Write, Edit, Glob, Grep | ファイル変更・コマンド実行可能 |
Bash をさらに制限したい場合はパターン指定が可能:
--allowedTools "Bash(git *),Bash(npm run *),Read,Write,Edit,Glob,Grep"
Self-hosted Runner のリスク
Self-hosted Runner は自分のマシン上で任意のコードを実行する。パブリックリポジトリでの利用は推奨されない (他者の PR がトリガーになりうる)。プライベートリポジトリ、または今回のようにオーナー制限付きのワークフローで使うのが安全。
コスト
| 項目 | コスト |
|---|---|
| GitHub Actions (self-hosted) | 無料 (自分のマシンを使うため) |
| Claude Code (Max プラン) | 月額のプラン料金に含まれる |
| Claude Code (API キー) | 実行量に応じた従量課金 |
| ヘルスチェック | 月1回、最小プロンプトなので実質ゼロ |
まとめ
- 構築時間: 半日程度 (デバッグ含む)
- 最大のハマりポイント: launchd の PATH 問題と Max プランの OAuth 認証
- 最も価値があること: iPhone から Issue 1つ書くだけで、Mac の Claude Code がコードを読んで分析・修正・PR 作成まで自動でやってくれる
- 2ステージ承認: 読み取り専用で調査 → 計画を確認 → 承認して実行、という流れで安全性を確保
同じ仕組みを作りたい方は、ワークフローファイルをコピーして、Runner のインストールと Secret の設定だけで動かせます。ハマりポイントの解決策は上に全て書いたので、同じ轍を踏まずに済むはず。
Built with Claude Code on macOS (Apple Silicon)