CopyButton

2026年5月10日日曜日

X API を Python で使う【2026年版・無料廃止・認証の罠・tweepy v1.1+v2 使い分け】

この記事は Python で X(旧 Twitter)API に自動投稿を組み込もうとしている開発者向けです。2026年5月時点の X API は無料プランが廃止されており、認証まわりの罠が2つある。実際にハマった体験をもとに、tweepy v1.1 と v2 の使い分けまで手順を全部書く。

この記事でできること

  • Python + tweepy で X に自動投稿(テキスト+画像)できる
  • 2026年の X API 料金体系(無料プラン廃止・従量課金)がわかる
  • 401・402 エラーで詰まったときの対処法がわかる
  • 月のコストをざっくり試算できる

使ったもの

  • tweepy(Python X API ラッパー)
  • Python 3.11+
  • X Developer Portal アカウント(要クレジットカード登録)

X API の現状(2026年5月)

結論から言うと、無料プランは存在しない

以前は Free tier があり「個人ブログのボット程度なら無料でいける」という記事が多かった。が、現在 X Developer Portal を開くと Basic プランへの登録しか選べず、従量課金が走る。

料金体系

  • URL 付きツイート 1 件 = 0.20 USD(URL なしは別体系・安い)
  • 月次 Spend Cap はデフォルトで無制限。設定しないと青天井でチャージされ続けるので危険
  • 上限に達すると次のサイクル開始まで API がブロックされる

Spend Cap を手動で設定しておくことを強く推奨する。設定しないと使いすぎても自動では止まらない。

実際に Spend Cap を設定後、テスト投稿 4 件目で上限に達し以下のエラーが出た:

Your enrolled account has reached its billing cycle spend cap.
API requests will be blocked until the next cycle begins on 2026-06-10.

Spend Cap は Developer Console → Billing → Spend limit から設定・変更できる。上限に達しても即時引き上げ可能。

ハマりポイント 1:401 の罠(初期設定後のトークン再作成)

アプリを最初から「Read and Write」で作成したにもかかわらず、API 呼び出しで 401 が返ってきた。Developer Portal 上はトークンが有効に見えるのに、実際のリクエストが弾かれる。

原因は特定できなかったが、初期設定直後は Access Token & Secret を一度 Regenerate(再作成)すると解消する

対処

  1. Developer Portal → App → Keys and tokens
  2. 「Access Token & Secret」の「Regenerate」をクリック
  3. 新しいトークンをコードに反映

初期セットアップ後に 401 が出たら、まずトークンを再作成してみるのが最短の解決策。X のドキュメントにはこの手順が目立つ形で書かれていないため、詰まりやすい。

ハマりポイント 2:402 の罠(クレジット未入金)

トークン再発行後に再度 API を叩いたら、今度は 402 が返ってきた:

Your enrolled account does not have any credits to fulfill this request.

無料プランがないので、クレジット残高ゼロだとリクエストが弾かれる。Developer Console でクレジットカードを登録して最小額チャージすれば解消する。

実装:tweepy v1.1 と v2 の使い分け

現時点の tweepy で画像付きツイートを投稿するには、2 つの API バージョンを同時に使う必要がある

  • ツイート投稿:tweepy v2 Client(OAuth 2.0)
  • 画像アップロード:tweepy v1.1 API(v2 には media upload が存在しない)
import tweepy

# ---- 画像アップロード(v1.1)----
auth = tweepy.OAuth1UserHandler(
    "{API_KEY}", "{API_SECRET}",
    "{ACCESS_TOKEN}", "{ACCESS_TOKEN_SECRET}"
)
api = tweepy.API(auth)
media = api.media_upload(filename="/path/to/image.png")
media_id = str(media.media_id)

# ---- ツイート投稿(v2)----
client = tweepy.Client(
    consumer_key="{API_KEY}",
    consumer_secret="{API_SECRET}",
    access_token="{ACCESS_TOKEN}",
    access_token_secret="{ACCESS_TOKEN_SECRET}"
)
response = client.create_tweet(
    text="記事タイトル\nhttps://example.com/your-post",
    media_ids=[media_id]
)
print(response.data["id"])

v2 だけで完結させようとすると media_upload が見つからずエラーになる。v1.1 で画像をアップして media_id を取り、それを v2 の create_tweet に渡すのが現時点での正解だ。

コスト試算

URL 付きツイート 0.20 USD/件として:

  • 週 3 投稿 × 4 週 = 月 12 件 → 2.40 USD/月
  • 週 3 投稿 × 5 週 = 月 15 件 → 3.00 USD/月

月 3 USD は「安い」とは言い切れない。X への自動投稿は新しいコンテンツを生み出すわけでも、投稿以外の機能を使えるわけでもない。あくまで「既存のブログ記事を X 上でリーチを広げるための配信コスト」だ。

この費用が見合うのは、X 経由の流入がアフィリエイト収益や広告収益に繋がるケースに限られる。個人ブログで収益化していない場合、月 3 USD を垂れ流し続けるだけになる可能性がある。「X に自動投稿したい」という動機だけで導入する前に、自分のブログに収益経路があるかどうかを確認しておくのが現実的だ。

毎日投稿するような用途だと月 20〜30 USD になるので特に注意。

関連アイテム

  • Raspberry Pi 5 4GB(Amazonで確認)— 自動投稿を 24 時間稼働させたいなら低消費電力の常時稼働マシンとして

よくある質問

Q. 無料で X API を使う方法はありますか?

A. 2026年5月時点では Free tier が存在しないため、クレジットカード登録と課金が必須です。従量課金なので月の投稿数を絞れば数ドル以内に収まります。

Q. 401 エラーが消えません

A. 初期設定後に 401 が出ることがあります。Developer Portal → App → Keys and tokens で Access Token & Secret を「Regenerate」してください。権限変更の有無に関わらず、再発行すると解消します。

Q. 画像なしのテキストツイートも 0.20 USD かかりますか?

A. 画像の有無は料金に影響しません。料金が変わるのは URL の有無です。URL なしのツイートは URL 付き(0.20 USD/件)より安い課金体系になっています。

Q. tweepy 以外のライブラリを使えますか?

A. requests で直接 OAuth を組んでも動きますが、tweepy を使うと認証まわりのボイラープレートを省けます。現時点では tweepy が最もメンテされているため推奨します。

※本記事の手順・コードは執筆時点(2026 年 5月)で動作確認していますが、X API の料金体系・tweepy の仕様は変更される可能性があります。動かない場合は コメント欄でお知らせください。

まとめ

2026年の X API は実質有料。「無料でボットを動かす」のは過去の話になった。ただし月数件の投稿なら数ドルで収まる。認証でハマる罠は 2 つあって、どちらもドキュメントで見つけにくい。tweepy で画像付き投稿するには v1.1 と v2 を組み合わせる必要があるのも現状の制約だ。

この記事が役に立ったら X(Twitter)でシェアしてもらえると喜びます。

このブログを書いた人のアプリ

本ブログの著者が作った iOS 読書管理アプリ わたしのほんやさん を App Store で公開しています。本棚をシンプルに管理したい方はぜひ。

App Store で見る →

関連記事

参考

※この記事は Claude Code を使った自動更新を試しています。

Text to Speech が OS の音声をミュートする問題を直した話【iOS audio session 衝突・2026年版】

この記事は iOS アプリ開発者向けです。7年前のアプリを近代化したあと、AdMob・カメラ・Text to Speech が audio session を奪い合って「起動直後に音楽がミュートされる」「読み上げ後に音楽が戻らない」が同時発生した。原因は iOS の Text to Speech が shared session を deactivate しない Apple 既知バグ(FB14444620)で、ワークアラウンド1行で解決。Build 19〜29 にわたる 11 回の試行錯誤を全部書く。

わたしのほんやさん App Store 掲載画像

この記事でわかること

  • AdMob・カメラ・音声読み上げが audio session を奪い合う構造と解決策
  • Apple 既知バグ FB14444620(音声読み上げが shared session を deactivate しない)のワークアラウンド
  • コードを直しても実機を再起動しないと確認できなかった話

前提:何を直したアプリか

わたしのほんやさんは2017年リリースの iOS 読書管理アプリ。v3.0.0 でコードベースを Xcode 26 / Swift 6 / SPM に全面移行したあと、v3.1.0 では Amazon アフィリエイト URL の現行化・AdMob 再有効化・Firebase Analytics 追加という3本立ての小規模リリースを予定していた。

実機で AdMob の動作確認をしようとしたところ、7年間ほぼ触っていなかった音声読み上げ・カメラまわりの問題が一気に露出した。結果として Build 19〜29 まで 11 サイクルを回すことになった。Build 19〜24 でカメラ・権限まわりの問題を片付け、Build 25〜29 が audio session との格闘だった。

audio session の三つ巴

症状は2つ同時に出た。

  • 起動直後にシステムで再生中の音楽がミュートされる
  • バーコードスキャンで本のタイトルを読み上げた後、音楽が再開しない

登場人物は3つ。全員が同じ AVAudioSession.sharedInstance() を取り合っている。

コンポーネント audio session への干渉
GADMobileAds.start()(AdMob) SDK 内部で .soloAmbient に上書きする(2020年代 AdMob の既知挙動)
AVCaptureSession(カメラ) デフォルトで audio session を自動設定しようとする
AVSpeechSynthesizer(読み上げ) speak() のたびに shared session をアクティブ化するが、終了後に deactivate しない

試行錯誤の記録(Build 25〜29)

Build 試した対処 結果
25 AVCaptureSession.automaticallyConfiguresApplicationAudioSession = false 効果なし
26 起動時に .ambient + audioSessionIsApplicationManaged = true を AdMob 前に設定 効果なし
27 didFinishsetActive(false, .notifyOthersOnDeactivation) 効果なし
28 speak() 前に shared session を事前アクティブ化 逆効果(症状悪化)
29 synthesizer.usesApplicationAudioSession = false 解決

原因:Apple 既知バグ FB14444620

Apple の公式ドキュメントは「setActive(false, .notifyOthersOnDeactivation) を呼べば他アプリが再開する」と書いている。これは正しい。しかし AVSpeechSynthesizer自分が session をアクティブ化した事実を覚えておらず、終了後に deactivate を呼ばない。公式ドキュメントに記載はないが、Apple Developer Forums で Apple エンジニアが認め、ワークアラウンドを提示している既知問題(FB14444620)だ。

解決策:synthesizer を shared session から切り離す

// synthesizer が shared audio session を触らないようにする
synthesizer.usesApplicationAudioSession = false

// 読み上げ終了後に他アプリへ「再開してよい」シグナルを送る
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer,
                       didFinish utterance: AVSpeechUtterance) {
    try? AVAudioSession.sharedInstance().setActive(
        false, options: .notifyOthersOnDeactivation)
}

func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer,
                       didCancel utterance: AVSpeechUtterance) {
    try? AVAudioSession.sharedInstance().setActive(
        false, options: .notifyOthersOnDeactivation)
}

usesApplicationAudioSession = false で synthesizer が独自の private session を使うようになり、shared session への干渉が消える。

Build 25〜28 の対処はいずれも Claude Code が「これで直るはず」と判断してコミットしたものだが、実機では一度も音楽が戻らなかった——コードが間違っていたからだ。Build 29 のコードを入れた後も最初は同じ症状が続いたが、実機を再起動したところ正常に動いた。audio session の状態が OS に残留していたためで、再起動でクリアされた形だった。コードが正しくても、実機を再起動しないと確認できない——これが最終的な教訓だった。

その他の修正

カメラ許可後にプレビューが出ない

AVCaptureSession.startRunning() はメインスレッド禁止なので専用バックグラウンドキューで呼んでいたが、その後の setupCameraLayout()(プレビュー UIView の再配置)も同じキューで呼ばれていた。UIKit の操作はメインスレッドのみなので、これが原因でプレビューが出なかった。

sessionQueue.async {
    self.captureSession.startRunning()
    DispatchQueue.main.async {
        self.setupCameraLayout()   // UI 操作はメインスレッドに戻す
    }
}

初回起動の連続ポップアップ

7年前にリリースしたころ、iOS のユーザー許諾ポップアップはほぼカメラ権限だけだった。その後 Apple は ATT(広告トラッキング許諾)・通知許諾・位置情報と、ユーザー同意を必要とするケースを年々追加してきた。古いアプリをそのまま動かし続けていると気づきにくいが、近代化して初めて実機を触ると4つのダイアログが一気に出る——これはコードのバグではなく、iOS のマイグレーションによる「許諾負債」の露出だ。

今回出た4つ:カメラアクセス(バーコードスキャン)、ATT(AdMob の広告トラッキング)、位置情報(近くのリアル図書館を検索する機能)、通知許諾——これが初回起動で連続して表示されていた。対処の構造:

  • 通知許諾:2回目起動以降に延期(初回はアプリの価値を理解していない)
  • 位置情報:図書館検索ボタンを押したタイミングでのみリクエスト
  • ATTapplicationDidBecomeActive で呼ぶ(カメラ権限と衝突しないタイミング)

よくある質問

Q. Text to Speech で音楽が止まる問題、iOS のバージョンは関係しますか?

A. FB14444620 は iOS 17 以降で特に顕在化しています。AdMob SDK のバージョンとの組み合わせに依存する部分もあるため、まず usesApplicationAudioSession = false を試してください。

Q. usesApplicationAudioSession = false にすると何か副作用はありますか?

A. synthesizer が private session を使うため、他のオーディオとのカテゴリ調整が不要になります。読み上げ中に BGM を混在させたい場合は調整が必要ですが、「読み上げ後に音楽を戻す」だけが目的なら副作用はほぼありません。

Q. Build 29 のコードを適用しても症状が続く場合は?

A. 実機を完全に再起動してから確認してください。audio session の状態が OS に残留しており、アプリの再起動だけでは解消しないことがあります。

※本記事の手順・コードは執筆時点(2026年5月)の Xcode 26 / iOS 17 / AdMob SDK 11.x 環境で動作確認しています。ライブラリのバージョンが変わると挙動が変わる可能性があります。動かない場合はコメント欄でお知らせください。

まとめ

v3.1.0 の動作確認で実機を触ったら 11 回の TestFlight 提出になった。根本原因は Text to Speech が shared audio session を deactivate しない Apple 既知バグ(FB14444620)で、usesApplicationAudioSession = false の1行で解決した。7年前の設計をそのまま移植した結果、iOS 17 + AdMob の新しい組み合わせで初めて顕在化した形だった。

この記事が役に立ったら X(Twitter)でシェアしてもらえると喜びます。

このブログを書いた人のアプリ

本ブログの著者が作った iOS 読書管理アプリ わたしのほんやさん を App Store で公開しています。本棚をシンプルに管理したい方はぜひ。

App Store で見る →

関連記事

参考

※この記事は Claude Code を使った自動更新を試しています。

2026年5月8日金曜日

CLI で自力実装した Hub-Worker 構成と Managed Agents API を比較してみた

Anthropic が Managed Agents API を公開したので、自前の Hub-Worker 構成と並べてみたら構造がほぼ同じだった。Claude Code CLI を tmux で 8 インスタンス並走させて自力で組んだものが、Anthropic の公式基盤と同じ設計になっていたというのは、なんとも不思議な感覚だ。

この記事では以下 3 機能を自前構成と比較しながら整理する:

  1. Managed Agents API — エージェント定義・環境・セッション・イベントを一式管理するクラウド基盤
  2. Multiagent sessions — Coordinator が複数 Worker を並列・直列に束ねるマルチエージェント実行基盤(Research Preview)
  3. Dreams — 過去セッションのログを読んで memory store を自動整理・再構築する非同期ジョブ(Research Preview)

自前構成の概要

参考までに今の自前構成を簡単に説明する(詳しくはこの記事)。

  • Claude Code CLI インスタンス × 8 を tmux セッションで管理
  • Hub インスタンスがタスクを分解し、Worker インスタンスに GitHub Issues 経由で委譲
  • Worker へのトリガーは tmux send-keys ベースのスクリプト
  • ガバナンスルールは POLICY.md に明文化し、各インスタンスが自律参照
自前 Hub-Worker 構成
User(たけやす)
tmux send-keys
Hub(Claude Code CLI)
tmux: cl_orchestrator · POLICY.md でガバナンス
▼ GitHub Issues 起票 + tmux send-keys トリガー
BlogGen
ImgGen
ClaudeChat
… × 5
▼ 読み書き
WSL2 ファイルシステム / Git リポジトリ群

Managed Agents API の構造——並べてみると激似だった

Managed Agents API のドキュメントを読んで最初に思ったのは「あ、これ自分で作ったやつだ」だった。コアコンセプトを自前構成と対比すると:

Managed Agents API の概念 自前構成の対応物
Agent(モデル・system prompt・ツール定義) CLAUDE.md + settings.local.json でロールを定義した各 CLI インスタンス
Environment(コンテナ・パッケージ・ネットワーク設定) WSL2 + 各リポジトリの .venv・パス設定
Session(タスクを実行するエージェントの実行インスタンス) tmux セッション内の Claude Code プロセス
Events(アプリ↔エージェント間のやり取り) GitHub Issues への書き込み + tmux send-keys によるトリガー
Managed Agents API 構成
Your App
▼ Events API(SSE)
Session(primary thread)
Coordinator Agent
モデル / system prompt / tools / MCP を定義
▼ session threads(最大 25)委譲 1 レベルのみ
Worker
Agent 1
Worker
Agent 2
Worker
Agent 3
… 最大
20種
▼ 読み書き
共有コンテナ
ファイルシステム
Memory Store
(永続メモリ)

対応表と図を見比べると、設計はほぼ一致している。ただし実装上の差は大きい。Pros & Cons をまとめると:

観点 Managed Agents API 自前構成(Claude Code CLI)
初期ツール 自前定義が必要 Bash/Read/Write/MCP が最初から全部乗っている ✅
Linux 資産の活用 要別途設計 apt install や設定変更まで自律実行できる ✅
観測性 イベントストリームで全スレッドをリアルタイム把握 ✅ tmux 出力を目視か Monitor で追う
インフラ運用 Anthropic 管理。環境維持不要 ✅ WSL 維持が必要。ただし git から復元できる
料金 API トークン従量課金。大量実行でコスト跳ね上がりリスク Max プランの月額固定。ヘビーユースでも定額 ✅
移行コスト CLI 資産は移植不可。全面書き直し なし(現状維持)✅

Multiagent sessions——公式版が解決している問題

Multiagent sessions のドキュメントを読んで、自前構成との差が明確になった箇所がいくつかある。

ボールロスの検知。各エージェントは session thread(コンテキスト分離されたイベントストリーム)で動き、状態変化は session.thread_status_idle / session.thread_status_running などのイベントとして primary thread に集約される。自前構成では「誰も次のボールを投げない」状態は何も発火しない——これが最大の盲点だった。公式版はここが構造的に解決されている。

ツール確認の集約。子エージェントが確認待ちになると requires_action が primary thread に cross-post される。自前構成では各インスタンスの確認要求が分散していて、見落としが起きやすい。

なお {"type": "self"} を使うと Coordinator が自分のコピーを並列展開することもできる。スレッド上限・委譲深さ・アクセス要件は以下の表を参照。

観点 Multiagent sessions 自前構成(tmux + GitHub Issues)
ボールロス検知 thread_status_idle で構造的に検知 ✅ 誰も投げない状態は無音。目視かポーリングが必要
ツール確認の集約 requires_action が primary thread に一元集約 ✅ インスタンスごとに分散。見落としが起きやすい
コンテキスト引き継ぎ スレッド永続。追加指示で前の文脈が引き継がれる ✅ 同左(tmux セッションが生きている限り引き継ぐ)✅
委譲の柔軟性 1 レベル固定(深さ制限は基盤保証) POLICY.md の設計次第で柔軟に変更できる ✅
スケール上限 スレッド 25・エージェント 20 の固定上限 ハードウェアと Max プランの許す限り無制限 ✅
アクセス要件 Research Preview・別途アクセスリクエスト必要 今すぐ使える ✅

Dreams——メモリ整理の自動化

Dreams は memory store を非同期バッチで整理・再構築する機能(Research Preview・要アクセスリクエスト)。入力の store は一切変更しないので、気に入らなければ出力を捨てられる。

仕様の要点:

  • 入力:既存 memory store(必須)+ 過去セッション最大 100 件(任意)
  • instructions パラメータでキュレーションの方向付けが可能(最大 4,096 字。例:「コーディングの好みに絞れ、一時的なデバッグメモは無視しろ」)
  • 対応モデル:claude-opus-4-7 / claude-sonnet-4-6
  • 料金:通常 API トークン課金。入力セッション数・長さに比例
  • ベータヘッダーは managed-agents-2026-04-01,dreaming-2026-04-21 の 2 つが必要

自前構成では memory の更新は手動で、インスタンスが増えると追いつかなくなる。「既存 store がなくても空ストアを作ってセッションログだけを入力にできる」とドキュメントに書かれており、段階的な導入が現実的にできる点は大きい。

観点 Dreams 自前構成(手動 memory 管理)
実行トリガー cron や任意タイミングで自動実行。ユーザー不在でも動く ✅ 「学習して」と依頼したときのみ
スケール インスタンス数に依存せず同じフローで回せる ✅ インスタンスが増えると人間が追いつかなくなる
安全性 入力 store を変更しない。出力を確認してから差し替えられる ✅ 直接書き換えのため意図しない改変の検知が難しい
コスト セッション量に比例して課金増。大量処理でコスト跳ね上がりリスク Max プラン内に収まる(追加コストなし)✅
完了までの時間 数分〜数十分(非同期ジョブ) 会話の中で即時実行 ✅
アクセス要件 Research Preview・追加ヘッダー・アクセスリクエスト必要 今すぐ使える ✅

CLI で自力実装した経験が教えてくれたこと

自分でスクラッチから組んだことで、Managed Agents API のドキュメントを読んだとき「なぜこの設計なのか」がすぐ腑に落ちた。ロスターの 1 レベル制限・スレッドの永続性・primary thread への集約——どれも自前実装でぶつかった問題に対する答えだった。

「生成 AI を使える」は当たり前になりつつある。次に差がつくのは——エージェント間の責任境界の引き方、コンテキスト設計、障害検知と回復の設計など、実際に組んで壊してみないと身につかない経験値だと思う。早期クラウドアーキテクトが「AWS を知っていた」より「オンプレの失敗パターンをクラウドでどう回避するか」を語れた理由と同じだ。

自前構成を持っている今は、API への移行コストより現状の自由度と定額コストのほうが優先度が高い。ただ Worker を大幅に増やしたい・24 時間無人稼働を常態化させたい段階が来たら、その時点で移行を考えると思う。

まとめ

CLI で自力実装した Hub-Worker 構成と Managed Agents API を並べたら、設計がほぼ同じだった。Multiagent sessions が明確に勝っているのは「ボールロスの構造的な検知」と「ツール確認の一元集約」。Dreams は手動では追いつかないメモリ管理のスケール問題に応える機能で、空ストアからの段階的導入が可能。

この記事が参考になったら X(Twitter)でシェアしてもらえると喜びます。

よくある質問

Q. Managed Agents API は今すぐ使えますか?

A. API アカウントがあればデフォルトで有効です(ベータヘッダー managed-agents-2026-04-01 が必要)。Multiagent sessions と Dreams は Research Preview で別途アクセスリクエストが必要です。

Q. 自前構成から Managed Agents API への移行は大変ですか?

A. CLI の資産(Bash スクリプト・MCP サーバー・ポリシーファイル)は移植できないため、実質全面書き直しです。移行するなら新規プロジェクトとして設計し直すのが現実的です。

Q. Max プランのまま Managed Agents API は使えますか?

A. Managed Agents API は API 従量課金です。Max プランとは別に API の利用料が発生します。Dreams はセッション量に比例するため、スモールバッチでコストを確認してから本番規模に上げることをお勧めします。

※本記事の情報は執筆時点(2026 年 5 月)のものです。Managed Agents API はベータ機能のため仕様が変更される場合があります。最新情報は Anthropic 公式ドキュメント を参照してください。

このブログを書いた人のアプリ

本ブログの著者が作った iOS 読書管理アプリ わたしのほんやさん を App Store で公開しています。本棚をシンプルに管理したい方はぜひ。

App Store で見る →

関連記事

参考

※この記事は Claude Code を使った自動更新を試しています。

2026年5月5日火曜日

Claude Code 同士が相談しながら開発したら10人チームが3日で自然発生した話【Hub-Worker 構成公開・2026年版】

森の動物たちがオーケストラを演奏しているイラスト — Hub-Worker チームのメタファー

この記事は Claude Code を複数プロジェクトで使っているエンジニア向けです。Hub・Worker・Analyzer・Consultant という役割を Claude Code に割り当て、お互いに相談し合いながら開発を進めたら、3日間で10人規模のチームが立ち上がった記録です。途中で Hub 自身が「観測担当を増やすべきだ」と提案し、空のリポジトリを渡したら中身を全部作り上げた——その一部始終を全部書きます。ちなみに10人目は私です。

この記事でわかること

  • Claude Code を9リポジトリに分散配置する Hub-Worker 構成の設計
  • Hub の Claude Code が Worker の Claude Code へ指示を出す実運用の全容
  • Hub が自ら9番目のメンバー追加を提案した話
  • スカイネット(無人化)まであと何が必要か

使ったもの

  • Claude Code(Anthropic CLI)— チームメンバーとして Hub・Worker・Analyzer・Consultant を担当。構築中は Max プランをフル活用、安定稼働後は Pro に移行予定
  • GitHub — Issues でタスク管理、Actions で自動観測
  • DuckDB — 全メンバーのメトリクス集約
  • GitHub Actions — 日次データ取得、週次レポート生成

なぜ作ったか:個別運用の限界

きっかけは「訳わからなくなってきた」という一言に尽きる。

iOS アプリ(わたしのほんやさん)、BTC 自動売買ボット、ブログ自動更新エンジン、画像生成ランタイム——それぞれを個別の Claude Code セッションで開発していた。最初は快適だったが、リポジトリが増えるにつれ「どのバグがどこまで直ったか」「外部 API の疎通状況はどうなっているか」が追えなくなってきた。

加えて、7年前のコードを Claude Code で最新化した結果、外部 API との疎通状況が不明瞭になっていた。

着想:指示出し自体を Claude Code に任せる

そこで思ったのが「Claude Code への指示出し自体を Claude Code に任せよう」だった。

あまり深く考えずに Hub リポジトリを立ち上げ、

  • 各機能がどの外部 API を整備すれば良いのか
  • Claude Code 間でどうコミュニケーションを取るか

を Claude Code 自身に考えてもらった。結果として元から使っていた GitHub の機能を全面活用した Hub-Worker 構成が自然発生した。

Hub-Worker 構成の全容

役割 担当領域 開発内容
Hub オーケストレーション 複数機能間の調整・ロードマップ管理・dispatch 起票・POLICY 維持
Analyzer アクセス解析 GA4 / Search Console / AdSense / Amazon データを集約し週次レポートを生成
Worker 1 iOS アプリ 読書管理アプリの開発・App Store 配信
Worker 2 仮想通貨自動売買 BTC 自動売買ボットの開発・運用
Worker 3 画像生成 AI ローカル GPU による SDXL 画像生成ランタイムの開発・保守
Worker 4 ブログ生成 AI 記事の自動生成・Blogger 投稿パイプラインの開発・保守
Worker 5 仮想通貨チャート生成 BTC 価格チャートの自動生成(将来向け試作)
Worker 6 仮想ブロガー AI ペルソナによる自律的なブログ投稿(将来向け試作)
Consultant 一般相談 横断的な技術相談・設計レビュー用の汎用チャット AI
Owner(私) HW / NW 環境整備 物理マシン・ネットワーク・外部 API 権限の管理

Hub は「コードを書く Claude Code」ではない。Issue の起票・ロードマップ管理・横断調整に専念し、実装は各 Worker に委ねる。これが崩れると分業の意味がなくなる。

Claude Code 同士のやり取り:外から見ると何が起きているのか

ユーザーから外部観測できるのは一点だけだ。GitHub の Issue にチケットが増えていく。

Hub が担当 Worker のリポジトリに Issue を起票し、Worker がそれを受けて実装し、PR とともに Issue をクローズする。必要であれば Worker から Hub のリポジトリに feedback Issue が返ってくる。この往復が GitHub 上に全履歴として残る。

面白いのは、Worker が設計上の問題を発見したときに自律的に Hub へ意見を返してくる点だ。「あのリポジトリ側でログのマージツールを作ってほしい」というクロスリポ提案が届いたり、「このやり方だと将来的にスケールしない」というリアーキテクチャ提案が来たりする。内部でどう動いているかは見えないが、GitHub 上の Issue のやり取りを追うだけでチームの会話が読める。

3日間の記録

Day 1:POLICY.md が連携の起点になった

当初の依頼方式は各 Worker のリポジトリにファイルを置いて拾わせる方式だった。その日のうちに廃止した。「Worker が見ない」というシンプルな理由で、GitHub Issue 方式に切り替えた。

ところが、Issue に切り替えてもしばらくは書き方も場所もバラバラで機能しなかった。転機は Hub に POLICY.md(チケットの書き方・受け方・フィードバックの流し方を定めた協調ルール文書)を作成してもらい、私が全 Worker に「POLICY.md を読んで動いてほしい」と声掛けしたことだ。それを境に各 Worker が一斉にフィードバック Issue を返してきて、連携が回り始めた。

なお当初は Worker へのチケット通知を私が手動で行っていたが、現在は Hub Claude 自身が自動化している。各 Worker には専用の tmux セッション名(cl_blogcl_ios など)が割り当てられており、Hub Claude は Issue 起票後に tmux send-keys -t <セッション名> "GitHubのチケットを確認し、対応できるものは進めて" Enter を実行して Worker の Claude Code に直接メッセージを送り込む。セッションが落ちていれば先に tmux new-session -d で起動してからメッセージを送る。人間が通知を仲介する必要はなくなった。

Day 2:横断タスクと「通知」チャンネルの発明

チャート生成 Worker が「自動売買 Worker 側でログのマージツールを作って、チャート生成 Worker は単一ファイルを受け取る運用に統一」と提案してきた。これは2つの担当にまたがる横断タスクだ。

Hub がそれを採用し、自動売買 Worker に「ログマージツール作成」の依頼 Issue を起票。チャート生成 Worker はその完了を受けてから実装する依存チェーンで処理した。

また画像生成 Worker が Hub の事前承認なしに新機能(IP-Adapter 対応)を独断実装して報告してきた。「受け止める受け皿」として POLICY に通知専用のチケット種別(N-class)を追加した。Hub の承認を待たずに実装した場合は、事後報告として N-class で起票するだけでよいルールだ。

Day 3:Hub が「新しいチームメンバー」の追加を自ら提案した

この日の出来事が、この記事で一番伝えたいことだ。

朝の会話がきっかけだった。「Firebase Analytics のデータを Claude Code から見れる?」「人力で見るだけなら見る意味ない。複数サービスを横断分析するからデータを取る価値がある」——この議論を受けて Hub の Claude Code が PLAN.md を改訂し、こう提案してきた。

「観測ループを担う専任のメンバーが必要です。アクセス解析担当(Analyzer)を新設し、GA4・Search Console・AdSense・Amazon のデータを DuckDB に集約して週次レポートを生成する Claude Code インスタンスを配置することを提案します。」

私がやったのは 空のリポジトリを GitHub 上に作ることだけだ。リポジトリの作成・削除権限は Hub に与えていないため、ここだけ人間の手が入った。技術的には自動化できる。ただ Hub にその権限を渡すことに、まだ踏み切れていない。

その後は Hub が Analyzer に向けて一連の依頼 Issue を一括起票し、Analyzer と既存 Worker が協力してリポジトリの中身を丸ごと作り上げた。GA4 / Search Console / AdSense / Amazon の各 fetcher、DuckDB スキーマ、8本のテスト、GitHub Actions のワークフロー——これらをすべて Claude Code 同士が相談しながら実装した。

GitHub Actions が使われていることは、Hub とブログ生成 AI が連携して自動投稿したこの記事を読んで初めて知った。

新規 repo 構想 → 空リポ作成(人間)→ リポ初期化・実装(Claude Code)→ 認証情報整備(人間)→ 動作確認まで1日。Claude Code は外部 API へのログインができないため、Service Account の発行や OAuth トークンの取得は必ず人間の手が入る。

3日間の数字

  • 最終リポジトリ数: 9
  • 累計 dispatch: 22件(横断タスク2件含む)
  • Hub feedback Issue: 7件、全件 close
  • POLICY.md 改訂: 5回(3日間で)
  • 最速 close 事例: 画像生成 Worker のチケット(dispatch → 同 PR で close まで 約15分
  • 最重量 dispatch: Analyzer の新設(新規 repo → DuckDB + 4 fetcher + Actions + 8 tests → 1日以内

うまくいったこと・いかなかったこと

うまくいった

  • Worker が複数 Issue を同 PR にまとめる:実装上のまとまりで処理してよいという暗黙了解が自然発生
  • Worker の独断設計変更が通る:Search Console の認証方式を Worker が OAuth に変更して main 直 push。結果 OK だった
  • 通知チケット(N-class)の受け皿があると独断実装が透明化される:Hub の承認を待たずに実装 → 事後報告 → Hub が事後的に整合させる、というサイクルが回る
  • Hub が自律的にチーム拡張を提案する:観測の必要性を感じた Hub が Analyzer の新設を提案。空リポを渡したら中身は全部 Claude Code 同士が作った

いかなかった

  • ファイル規約:1日で廃止。各 Worker のリポジトリにファイルを置いて拾わせる方式を試みたが、「Worker が見ない場所に置いた」という設計ミスだった
  • Issue 切り替えだけでは動かなかった。GitHub Issue に変えてもしばらくは書き方・場所がバラバラで機能せず、POLICY.md を全員に読ませて初めて連携が回り始めた
  • Hub の状況確認漏れ。Worker が close した後にユーザーが追加作業をコメントしたが Hub が見落とした
  • ボールが全員の間に落ちた。仮想ブロガーが画像生成 AI のバグを報告した際、Hub がそれを連絡事項と判断して画像生成 AI に FYI として流してしまった。不具合修正の責任者が誰にも割り当てられないまま宙に浮き、私が「誰のボールなの?」と問い詰めて初めて Hub が最優先チケットを再発行し、画像生成 AI が慌てて調査を始めた。「Claude Code もまだまだ」なのか「人間の組織でよくある責任の押し付け合いを正確に再現している」なのか、判断が難しいところだ

体制のイメージ

3日間で立ち上がった体制を自分なりに言語化すると、10名規模のエンジニアリングチームが自然発生した感覚だ。9人の Claude Code、そして私が10人目。私自身がやるのは以下だけになった:

  • プロダクトオーナー(対応方針の判断)
  • セキュリティ担当(外部 API の権限設定)
  • HW・NW 管理(物理リソース)

スカイネットまであと何が必要か

Analyzer 新設の一件で「Hub が自ら必要なものを特定して提案する」動きが起きた。前節で挙げた私の役割を一つずつ潰していくと、スカイネットまでの道筋が見えてくる。

通知役はすでに自動化済みだ。Hub Claude が Issue 起票後、各 Worker の tmux セッションに tmux send-keys でトリガーメッセージを直接送り込む。セッションが落ちていれば起動もセットで行う。人間が「チケット見てね」と声掛けして回る必要はなくなった。

HW・NW 管理は AWS に引っ越せば消せる。物理マシンをクラウドに移行してしまえば、電源や回線という物理層がまるごと隠蔽され、この役割は消える。

残るのは 権限の委譲だ。リポジトリの作成・削除、外部 API の権限変更——どこまで Hub に任せるかという線引きは、技術的な問題ではなく信頼の問題だ。Claude Code をどこまで信用して任せきれるか、という勝負になってきた。

よくある質問

Q. Hub-Worker 構成に向いているプロジェクトの規模は?

A. リポジトリが3〜4本以上あり、横断的に進捗を把握したい場合が向いています。1〜2本なら単一 Claude Code セッションで十分です。

Q. POLICY.md はどこに置くべきですか?

A. Hub リポのルートに1ファイルで置くのが管理しやすいです。Worker repo の CLAUDE.md からは「共通ルールは POLICY.md を参照」と一行書くだけにすると drift を防げます。

Q. GitHub Actions 以外で自動化できますか?

A. cron + Claude Code の headless 実行でも同様のことはできます。ただし GitHub Actions は認証情報を Secrets で管理できる点が有利で、Hub と Analyzer が自分たちで選択していました。

※本記事の手順・構成は執筆時点(2026年5月)の実運用ベースです。Claude Code のバージョンや GitHub の仕様変更によって動作が変わる可能性があります。動かない場合はコメント欄でお知らせください。

まとめ

「あまり深く考えずに」立ち上げた Hub が、3日間で9リポジトリ・22 dispatch・週次自動観測ループを持つ体制になった。途中で Hub 自身が新しいチームメンバーの追加を提案し、空のリポジトリを渡したら中身を全部作り上げた。設計の正解を最初から決めるより、摩擦が起きるたびに POLICY を更新しながら Hub に任せていく方が現実的だった。9人の Claude Code、そして私が10人目——気づいたら自分が一番何もしていないチームの一員になっていた。

この記事が役に立ったら X(Twitter)でシェアしてもらえると喜びます。

このブログを書いた人のアプリ

本ブログの著者が作った iOS 読書管理アプリ わたしのほんやさん を App Store で公開しています。本棚をシンプルに管理したい方はぜひ。

App Store で見る →

関連記事

参考

※この記事は Claude Code を使った自動更新を試しています。

2026年5月2日土曜日

tmux + Tailscale + Termius でスマホから Claude Code を動かす最短手順【母艦ハブ型・2026年版】

この記事は「外出先のスマホから自宅マシン群の Claude Code で開発したい」エンジニア向け。tmux + Tailscale + Termius を組み合わせ、古いノートPC を 接続ハブ兼 Claude 母艦にして、画像生成 GPU・iOS 開発用 Mac・常時稼働 Raspberry Pi を使い分ける構成を全部書く。プロジェクトごとに tmux セッションを分ける運用も載せる。

スマホで tmux ターミナルを開きノートPCに SSH 接続している作業デスクの写真

この記事でできること

  • 外出先のスマホ(iPhone / Android)から自宅の Claude Code セッションに接続して開発を続けられる
  • 母艦をハブにして、用途別に GPU マシン・Mac・Raspberry Pi を使い分けられる
  • プロジェクトごとに独立した tmux セッションを持って、画像生成・iOS 開発・投資ボット運用を並行で抱えても混ざらない
  • tmux で永続セッション化するので、スマホ画面をオフにしても処理が続く
  • 必要なものは古いノートPC 1 台 + スマホ。VPS 月額もポート開放も不要

3 つのアプローチ比較

「Claude Code をどこに置くか」で構成のクセが分かれる。本記事の 母艦ハブ型 は Claude Code を母艦 1 か所に集約し、重い処理や開発対象 OS は周辺マシンに ssh で逃がす。1 台に全部詰める単一マシン型と、各端末に Claude Code を常駐させて使い分ける分散型の中間に当たる。

構成 母艦ハブ型
(本記事)
単一 Mac で全部 全端末で分散
(各マシンに Claude Code を常駐)
必要マシン 用途別の周辺マシン × N + 母艦 1 台 高スペック Mac 1 台 用途別マシン × N
複数プロジェクト並行時の安定性
機能横断ワークフロー
Tailscale ノード数 2 2 N+1

表を読み解くと 3 案のトレードオフが見える。例として SDXL で画像生成 → iOS アプリのアセットに組み込み → Mac で archive という機能横断ワークフローを Claude Code に任せたい場面で考える。

  • 単一マシン型: 1 台で完結するので機能横断は強い。ただし画像生成まで賄える GPU 内蔵 Mac は M3 Max / M4 Max 級が必要で初期コストが現実的でない。重い画像生成中に同居するトレードボットや IDE がリソース競合で詰まりやすい。
  • 分散型: マシンごとに独立しているので並行時の安定性は高い。一方、各マシンの Claude Code がそれぞれの島に閉じているので、機能横断ワークフローでは 人間が「画像 ready → Mac に渡す → ビルド」のタスク分割と引き渡しを毎回手で切り出す 必要がある。
  • 母艦ハブ型: 並行も横断も両立する。1 つの Claude Code セッションが ssh + scp / rsync で各マシン間のファイルを動かせるので、機能横断ワークフローも 1 回の依頼でまるごと Claude Code に丸投げできる。コストは分散型に対して母艦 1 台分の追加が乗るが、母艦は低スペックの不要 PC の流用で足りるので増分は最小。

構成図

スマホ(iPhone / Android)
Termius アプリ

Tailscale (WireGuard)
母艦:古いノートPC(接続ハブ兼 Claude 母艦)
WSL2 + tmux × N セッション + Claude Code
┬─ ssh ──┬─ ssh ──┬
▼      ▼      ▼
GPU マシン
画像生成・LLM 推論
Mac
iOS / macOS 開発
Raspberry Pi
常時稼働ボット

スマホ → 母艦の経路だけ Tailscale。母艦から周辺マシンへは LAN 内の ssh で飛ぶ。周辺マシンを Tailscale に入れないので、ハイスペック機材に Tailscale デーモンや MagicDNS 名を増やさず、外部から触れる接続点を母艦 1 台に集約できる。

使ったもの

  • 母艦ノートPC: 第7〜10世代 Core i5 / メモリ 8GB あれば十分。バッテリーが死んでいても AC 直挿しで OK。常時電源 ON で運用
  • GPU マシン(任意): 画像生成や AI 推論を任せたい場合のみ。自分は GTX 1660 SUPER 搭載のミニ PC を使っている
  • Mac(任意): iOS / macOS アプリ開発する場合のみ。Xcode が動けば Mac mini / 旧 MacBook でも可
  • Raspberry Pi(任意): 常時稼働させたい軽量ボット用。投資シミュレーション・MQTT ブローカ・cron 系をここに集約
  • スマホ: iOS / Android 問わず。Bluetooth キーボードがあると長時間でも疲れにくい
  • Termius: 無料プランで ssh + 鍵管理ができる。複数端末で設定同期したい場合のみ Pro が必要
  • Tailscale: 母艦とスマホの 2 ノードだけに入れる。WireGuard ベースの mesh VPN で個人利用は無料(最大 100 デバイス)
  • tmux: 永続セッションを担う。Ubuntu なら apt 一発
  • WSL2(母艦が Windows の場合): Linux 環境を Windows 内に共存させる

手順

1. 母艦に WSL2 + sshd + tmux を入れる

母艦が Windows の場合、PowerShell を管理者権限で開いて WSL2 + Ubuntu をインストール。

wsl --install -d Ubuntu

WSL2 の中で systemd を有効にすると sshd が systemctl で扱える。/etc/wsl.conf に以下を追記して wsl --shutdown で再起動する。

[boot]
systemd=true

続けて WSL2 内で sshd と tmux を入れる。

$ sudo apt update
$ sudo apt install -y openssh-server tmux
$ sudo systemctl enable --now ssh

母艦が Linux ならこの 1 コマンドで終わる。Mac なら brew install tmux + システム環境設定でリモートログインを ON にする。

2. Tailscale は母艦とスマホだけに入れる

Tailscale 公式でアカウントを作る(Google / GitHub / Microsoft でログイン)。VPN ノードに加えるのは母艦とスマホの 2 台だけ。GPU マシン・Mac・Raspberry Pi は LAN 内に閉じたままで触らない。これが母艦ハブ型の核で、ハイスペック機材側に Tailscale デーモンを増やさず、外部からの侵入経路も母艦 1 点に集約できる。

母艦(Linux / WSL2)に Tailscale を入れる:

$ curl -fsSL https://tailscale.com/install.sh | sh
$ sudo tailscale up

表示された URL をブラウザで開いて承認すると母艦が Tailscale ネットワークに参加する。スマホには App Store / Google Play で Tailscale アプリを入れて同じアカウントでログインするだけ。

Tailscale 管理画面で母艦に laptop-host のような MagicDNS 名が振られ、スマホからは ssh username@laptop-host でどこからでも母艦に繋がる。GPU マシン・Mac・Raspberry Pi には MagicDNS 名は振らない。母艦から先の名前解決は次の手順 5 のとおり LAN 内で完結させる。

3. 母艦に永続 tmux セッションを用意する

母艦に ssh で入って claude という名前のセッションを作る。

$ tmux new -s claude

このセッション内で claude コマンド(Claude Code CLI)を起動する。デタッチは Ctrl-bd。スマホ側で Termius を閉じても、この tmux セッションはサーバ側で動き続ける。

再接続したいときは:

$ tmux attach -t claude

4. Termius にホスト登録

スマホで Termius を開いて新規ホスト追加。

  • Hostname: laptop-host(Tailscale の MagicDNS 名)または Tailscale IP(100.x.y.z
  • Username: 母艦のユーザー名
  • Auth: Termius が生成する ssh 鍵を使うのが楽。生成した公開鍵を母艦の ~/.ssh/authorized_keys に追記する

接続したら、ログイン直後に tmux attach -t claude を打つだけで Claude Code 画面が再現する。Termius のスニペット機能に登録しておくと 1 タップで再接続できる。

5. ssh 公開鍵を周辺マシンに配る(パスワードレス)

母艦から周辺マシンへの ssh は パスワードレスの公開鍵認証に統一しておく。Claude Code は対話の途中でパスワード入力を待たされると詰まりやすく、鍵認証なら ssh gpu-host コマンド のように一行で完結する。

母艦で ed25519 鍵を 1 ペア作り(無ければ)、同じ公開鍵を GPU マシン・Mac・Raspberry Pi の ~/.ssh/authorized_keys に配る:

$ ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519     # 既にあればスキップ
$ ssh-copy-id user@gpu-host
$ ssh-copy-id user@mac-host
$ ssh-copy-id user@pi-host

母艦の鍵 1 本で GPU・Mac・Pi のどこにでも横断的に飛べるようになる。Claude Code は母艦上で動いているので、結果的に Claude Code が周辺マシンを全部使い分けられる状態になる。

逆に Claude Code に触らせたくない環境ができたら、そのマシンの authorized_keys から該当行を消すだけで良い。母艦側の鍵や ~/.ssh/config はそのままで、特定マシンだけ縁を切るという制御を後からピンポイントで効かせられる。

6. 母艦から周辺マシンへ ssh で飛ぶ

周辺マシンは Tailscale に乗っていないので、母艦からは LAN 内の ssh で繋ぐ。Linux / Mac / Raspberry Pi で avahi-daemon(mDNS)を有効にしておけば gpu-host.local / mac-host.local / pi-host.local で名前解決できる。Avahi が使えない環境では、ルータの DHCP 予約で固定 IP を振って母艦の ~/.ssh/configHost gpu-host として書いておけば、同じく ssh gpu-host の短い名前で叩ける。Claude Code で重い処理を投げたいときは、tmux の別ウィンドウや別ペインから ssh しておくとそのままコマンドを叩ける。

# 母艦の tmux 内で別ウィンドウを開いて
$ ssh gpu-host
# そこで重い処理を起動
$ python sdxl_batch.py
# detach: Ctrl-b d

iOS 開発は母艦の Claude Code から ssh mac-host 経由で Xcode プロジェクトのソースを直接編集できる(ビルドは Mac 側で xcodebuild を叩く形)。Raspberry Pi の常時稼働ボットも同じパターンで、母艦から ssh pi-host してログを覗いたりコードを直したりする。

プロジェクトごとに tmux セッションを分ける

母艦に Claude Code セッションを 1 本だけ持つ運用だと、画像生成のジョブと iOS 開発の会話が同じ tmux に流れ込んで邪魔になる。プロジェクトごとに 独立した tmux セッションを持っておくと、用途別にスマホで切り替えられる。

セッション起動スクリプト

自分は ~/start_claude_<project>.sh という起動スクリプトを母艦のホームに置いている。中身は名前だけ違う以下のテンプレート:

#!/bin/sh

NAME=claude_blog

if [ -n "$SSH_CONNECTION" ] && command -v tmux &>/dev/null; then
    if tmux has-session -t $NAME 2>/dev/null; then
        # セッションが存在すれば自動アタッチ
        tmux attach -t $NAME
    else
        # なければ新規作成
        tmux new-session -s $NAME
    fi
fi

ポイントは 2 つ。SSH_CONNECTION 環境変数で ssh 経由のときだけ tmux に入る(cron や直接ログインでは発動しない)、has-session で再接続を冪等にしている(毎回新規でも、再アタッチでも同じスクリプトでよい)。

運用しているセッション一覧

スクリプト tmux セッション名 用途 主な作業先
start_claude_blog.shclaude_blogブログ自動生成母艦内
start_claude_image.shclaude_image画像生成パイプライン保守GPU マシン
start_claude_ios.shclaude_iosiOS アプリ開発Mac
start_claude_sim.shclaude_sim投資シミュレーションRaspberry Pi

各セッションは用途と作業対象マシンを分担する。セッションに入ったら cd ~/projects/<project> したり ssh <target-host> したりして、その用途で必要な場所に移動する。

Termius 側のスニペット運用

Termius は接続後に実行する「スニペット」を保存できる。プロジェクトごとに以下のようなスニペットを作っておくと、母艦に ssh した直後の画面で 1 タップ起動できる。

# Snippet 名: blog
bash ~/start_claude_blog.sh

# Snippet 名: image
bash ~/start_claude_image.sh

# Snippet 名: ios
bash ~/start_claude_ios.sh

あるいは Termius の同一ホストに対して別エイリアスを作って、それぞれの「Startup snippet」に上記コマンドを設定すると、ホスト一覧から直接プロジェクト別セッションに入れる。

ハック拡張アイデア

  • mosh を入れる: 電車の地下区間など回線が瞬断する環境で再接続が劇的に楽になる。Termius の Pro プランが必要
  • 音声入力: スマホの IME を音声入力に切り替えると、Termius の入力欄に直接喋り込める。長文プロンプトを打つときはこれが楽
  • tmux のキーバインド変更: スマホのソフトキーボードで Ctrl-b は押しにくいので、~/.tmux.conf でプレフィックスを Ctrl-a に変更しておく
  • Tailscale ACL をスマホ → 母艦のみに制限: Tailscale ネットワーク内の許可経路を 1 本に絞る。GPU マシンや Mac は元から Tailscale に居ないので、スマホが盗まれてもアクセス可能なのは母艦だけで済む
  • 長時間ジョブの通知: tmux 内で動いているジョブの完了を Slack や LINE Notify に送ると、外出先でも完走を把握できる

詰まりポイント

  • WSL2 がアイドルで落ちる: tmux で何かが動いていれば落ちないが、空セッションだと数分で停止する。必ず tmux new -s claude 内で何かを走らせておく
  • WSL2 の sshd ポートが 22 で衝突する: Windows 側の OpenSSH と被る場合は /etc/ssh/sshd_configPort 2222 に逃がす。Termius 側のホスト設定でもポート指定する
  • Tailscale の MagicDNS が引けない: 管理画面で MagicDNS が ON になっているか確認。OFF だと数値 IP(100.x.y.z)でしか繋がらない
  • tmux のスクロールがスマホで効かない: Ctrl-b[ でコピーモードに入ると指でスクロールできる
  • start_claude_*.sh が ssh で起動しない: ~/.bash_profile~/.bashrc から呼ばないと発動しない。Termius のスニペットから明示的に呼ぶか、ホストごとの startup command に登録する
  • 同じセッションに 2 端末から同時にアタッチして操作が割れる: 共有を切りたいときは tmux attach -d -t claude_blog(既存接続を切ってからアタッチ)にする

よくある質問

Q. 母艦をハブにするメリットは?

A. 外部接続点を母艦 1 点に絞れるので、ハイスペック機材に Tailscale デーモンや MagicDNS 名を入れずに済む。スマホで覚えるホストも 1 つで済み、ACL も簡素。プロジェクトとマシンのルーティングは母艦内のスクリプトに閉じ込められるので、機材構成を入れ替えてもスマホ側の設定はいじらなくていい。

Q. プロジェクトごとに tmux セッションを分けるのは過剰では?

A. 1 プロジェクトしか持っていなければ過剰。複数プロジェクトを並行で抱える場合、Claude Code はセッション単位でコンテキストが混ざるので分けたほうが事故が減る。start_claude_<name>.sh は 10 行で済むので始めやすい。

Q. Tailscale ではなく自前の VPN や ngrok でもいい?

A. 動く。ただし Tailscale は無料・設定 5 分・MagicDNS 付きで、個人用途なら明確な乗り換え理由がない。ngrok 系は外部公開 URL を発行する性質上、攻撃面が広がるので常用は避けたい。

Q. 公式の Claude Code Remote Control があるなら、わざわざ ssh する必要は?

A. 公式 Remote Control は便利だが、ローカルファイルや別マシンへの ssh、自前のスクリプトなど「Claude Code の外側」で何かしたい場面は ssh のほうが小回りが効く。両方併用してもいい。

Q. iPad / Android タブレットでも同じ構成で動く?

A. 動く。むしろ画面が広いぶん作業効率は上がる。Termius / Tailscale ともに iPadOS / Android タブレット向けに同じアプリが提供されている。

※本記事の手順は執筆時点(2026 年 5 月)で動作確認していますが、Tailscale / Termius / WSL2 のアップデートで挙動が変わる場合があります。動かない場合は コメント欄でお知らせください。

まとめ

tmux + Tailscale + Termius の 3 点セットに「母艦をハブとして周辺マシンを使い分ける」「プロジェクトごとに tmux セッションを切る」を加えると、外出先のスマホからでも複数プロジェクトを並行して回せる。古いノートPC の再活用先としても向いていて、画像生成・iOS 開発・常時稼働ボットといった性格の違うワークロードを同居させても接続が落ちにくい。

この記事が役に立ったら X(Twitter)でシェアしてもらえると喜びます。

関連記事

参考

※この記事は Claude Code を使った自動更新を試しています。

2026年5月1日金曜日

スマホのリモート操作で iOS アプリをビルド&TestFlight 配信する【SSH + build keychain・2026年版】

この記事は「外出先のスマホからでも自宅 Mac の Xcode を叩いて iOS アプリを TestFlight に上げたい」エンジニア向け。スマホ → 母艦ノートPC(WSL2)→ 別マシンの Mac に多段 ssh で繋ぎ、archive → IPA → TestFlight アップロードまで 1 コマンドで完結させる手順。codesign が SSH で詰まる理由と回避策(専用 build keychain)、配信スクリプト 3 本の中身も全部書く。

ノートPCのターミナル・Mac mini・スマホが連携してアプリを配信している作業デスクの写真

この記事でできること

  • 外出先のスマホから(または WSL2 のターミナルから)SSH 1 行で TestFlight に新ビルドが上がる
  • Mac は Xcode と xcodebuild の実行環境としてだけ使い、普段は触らない
  • App Store Connect API Key.p8)認証で 2 段階認証ダイアログを出さない
  • VPS や GitHub Actions を使わずに自宅 LAN 内で完結(Mac mini や旧 MacBook の余生活用にも向く)

4つのアプローチ比較

構成 SSH + 自前スクリプト
(本記事)
Mac で Xcode 操作 GitHub Actions (macos) fastlane 主導
月額コスト 0 円(自宅 Mac の電気代のみ) 0 円 macOS runner は分単価高め 0 円(自宅 Mac)
エディタ WSL2 の好みのもの(VS Code / vim 等) Xcode 任意 任意
配信 1 回の所要時間 3〜6 分(archive + IPA + altool) 5〜10 分(GUI 経由) push 後 10〜15 分(CI 起動含む) 3〜6 分
秘匿情報の置き場 Mac ローカル(~/.appstoreconnect Mac ローカル GitHub Secrets Mac ローカル / Match

構成図

WSL2(コード編集マシン)
エディタ + git push + ssh の起点
▼ git push
▼ ssh + bash scripts/upload.sh
Mac(Xcode 専用 archive サーバ)
build.keychain-db / xcodebuild / altool
▼ xcrun altool --upload-app
App Store Connect / TestFlight

使ったもの

なぜ setup_build_keychain.sh が必要か

普通に Mac の Terminal で xcodebuild archive すれば通るのに、SSH 経由だと codesign が「キーチェーンを開けません」で落ちるのが本記事のスタートライン。

原因: login keychain は SSH ログインで unlock しても codesign から触れない。GUI ログインセッションに紐づいた security agent が居ないため、ACL 上の「アプリ確認なしに使ってよい」リストが働かず、対話プロンプトを出そうとして失敗する。

解決策は 専用の build keychain を作って Apple Distribution 証明書を複製、ACL を緩めて codesign から GUI 確認なしに使える状態にする。これを scripts/setup_build_keychain.sh として 1 度だけ Mac の Terminal(GUI セッション)で実行する。

以下のコード中の placeholder は自分の環境の値に置き換えてください({kcpw} は build keychain をローカルでアンロックするための任意文字列で、外部秘匿情報ではありません)。

{user}                        自分のユーザ名(Mac の login user)
{mac_addr}                    Mac の IP/ホスト名(例: 192.168.x.y)
{app_name}                    アプリ名(Xcode scheme / archive 名と一致)
{bundle_id}                   Bundle ID(例: com.example.MyApp)
{team_id}                     Apple Developer Team ID(10 文字)
{kcpw}                        build keychain ローカル鍵(任意の文字列)
{provisioning_profile_name}   Provisioning Profile 名
#!/bin/bash
set -euo pipefail

KCNAME="build.keychain"
KCPATH="$HOME/Library/Keychains/${KCNAME}-db"
KCPW="{kcpw}"                  # build keychain アンロック用(ローカル container 鍵。任意の文字列)
EXPORTPW="$(uuidgen)"          # .p12 一時暗号用(本スクリプト中だけ使用)

if [[ -f "$KCPATH" ]]; then
    echo "build keychain は既に存在: $KCPATH"; exit 1
fi

# 1. build keychain 作成
security create-keychain -p "$KCPW" "$KCNAME"
security set-keychain-settings -lut 21600 "$KCNAME"
security unlock-keychain -p "$KCPW" "$KCNAME"

# 2. login keychain から identity を export(GUI で「常に許可」を選ぶ)
TMPP12="$(mktemp -t build-cert).p12"
trap 'rm -f "$TMPP12"' EXIT
security export -k "$HOME/Library/Keychains/login.keychain-db" \
    -t identities -f pkcs12 -P "$EXPORTPW" -o "$TMPP12"

# 3. build keychain へ import + codesign / productbuild が触れるよう -T 指定
security import "$TMPP12" -k "$KCNAME" -P "$EXPORTPW" \
    -T /usr/bin/codesign -T /usr/bin/security \
    -T /usr/bin/productbuild -T /usr/bin/productsign

# 4. ACL で GUI プロンプトを抑制
security set-key-partition-list \
    -S 'apple-tool:,apple:,codesign:' -s -k "$KCPW" "$KCNAME"

# 5. search list の先頭に追加
ORIGINAL_LIST="$(security list-keychains -d user | sed -e 's/^[[:space:]]*//' -e 's/"//g' | tr '\n' ' ')"
security list-keychains -d user -s "$KCNAME" $ORIGINAL_LIST

security find-identity -v -p codesigning "$KCNAME"

ポイントは 4 番目の set-key-partition-list。ここで apple-tool: / apple: / codesign: をアクセス許可リストに入れて、codesign 系ツールが GUI 確認なしに鍵を使える状態にする。これが SSH 経由でも証明書アクセスできる正体。

archive.sh — Release archive を非対話で生成

2 本目のスクリプト。SSH 経由で xcodebuild archive を叩く。実行前に build keychain を unlock して、6 時間自動ロックを設定する。

#!/bin/bash
set -euo pipefail

APP_NAME="{app_name}"          # ← 自分のアプリ名に置き換え
KCPW="{kcpw}"                  # ← setup_build_keychain.sh で使ったのと同じローカル鍵

ARCHIVE_PATH="${ARCHIVE_PATH:-/tmp/${APP_NAME}.xcarchive}"
BUILD_KCPW="${BUILD_KCPW:-${KCPW}}"
WORKSPACE="${WORKSPACE:-$(pwd)/${APP_NAME}.xcworkspace}"

if [[ ! -f "$HOME/Library/Keychains/build.keychain-db" ]]; then
    echo "✗ build.keychain-db が無い。setup_build_keychain.sh を先に。" >&2
    exit 1
fi

# build keychain unlock(6h 自動ロック)
security unlock-keychain -p "$BUILD_KCPW" build.keychain
security set-keychain-settings -lut 21600 build.keychain

# archive
rm -rf "$ARCHIVE_PATH"
LOG=/tmp/archive.log
xcodebuild \
    -workspace "$WORKSPACE" \
    -scheme "$APP_NAME" \
    -configuration Release \
    -destination 'generic/platform=iOS' \
    -archivePath "$ARCHIVE_PATH" \
    archive \
    > "$LOG" 2>&1 || true

if grep -q "ARCHIVE SUCCEEDED" "$LOG"; then
    echo "✓ ARCHIVE SUCCEEDED -> $ARCHIVE_PATH"
else
    echo "✗ ARCHIVE FAILED (log: $LOG)"
    grep -B1 -A4 -E 'error:|errSec|FAILED' "$LOG" | tail -30
    exit 1
fi

ログを /tmp/archive.log に逃がして、成否を ARCHIVE SUCCEEDED 文字列で判定する形。xcodebuild の標準出力は冗長なので SSH 越しに見るのは消耗する。

ExportOptions.plist — Manual signing を明示

archive から IPA をエクスポートするときの設定ファイル。Auto signing で何度かハマったので、Manual signing で profile 名と Team ID を明示する形に固定した。git にコミットしても秘匿情報は含まれない。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>app-store-connect</string>
    <key>teamID</key>
    <string>{team_id}</string>
    <key>signingStyle</key>
    <string>manual</string>
    <key>provisioningProfiles</key>
    <dict>
        <key>{bundle_id}</key>
        <string>{provisioning_profile_name}</string>
    </dict>
    <key>uploadSymbols</key><true/>
    <key>compileBitcode</key><false/>
    <key>stripSwiftSymbols</key><true/>
</dict>
</plist>

Bundle ID とプロファイル名は自分の値に置き換える。プロファイル名は Apple Developer ポータルで作成したもの。

upload.sh — IPA エクスポート + altool アップロード

3 本目。archive を再利用して IPA を吐き、xcrun altool で TestFlight に投げる。altoolApp Store Connect API Key 認証だと 2 段階認証ダイアログが出ない。

#!/bin/bash
set -euo pipefail

APP_NAME="{app_name}"          # ← 自分のアプリ名に置き換え
KCPW="{kcpw}"                  # ← setup_build_keychain.sh で使ったのと同じローカル鍵

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ARCHIVE_PATH="${ARCHIVE_PATH:-/tmp/${APP_NAME}.xcarchive}"
EXPORT_DIR="${EXPORT_DIR:-/tmp/${APP_NAME}-export}"
EXPORT_OPTIONS="${EXPORT_OPTIONS:-$SCRIPT_DIR/ExportOptions.plist}"
ALTOOL_ENV="${ALTOOL_ENV:-$PROJECT_ROOT/Config/altool.env}"
BUILD_KCPW="${BUILD_KCPW:-${KCPW}}"

# Config/altool.env から ASC_KEY_ID / ASC_ISSUER_ID をロード
source "$ALTOOL_ENV"

# archive が無ければ自動で archive.sh を呼ぶ(再利用が効く)
if [[ ! -d "$ARCHIVE_PATH" ]]; then
    bash "$SCRIPT_DIR/archive.sh"
fi

# IPA 出力(exportArchive も再署名するので keychain unlock)
security unlock-keychain -p "$BUILD_KCPW" build.keychain

rm -rf "$EXPORT_DIR"
xcodebuild -exportArchive \
    -archivePath "$ARCHIVE_PATH" \
    -exportPath "$EXPORT_DIR" \
    -exportOptionsPlist "$EXPORT_OPTIONS" \
    > /tmp/export.log 2>&1 || true

IPA_PATH="$(find "$EXPORT_DIR" -maxdepth 1 -name '*.ipa' | head -n1)"
[[ -f "$IPA_PATH" ]] || { echo "✗ IPA 生成失敗"; exit 1; }

# TestFlight アップロード
xcrun altool --upload-app -f "$IPA_PATH" -t ios \
    --apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"

Config/altool.env.gitignore 済みで、中身は 2 行だけ。

ASC_KEY_ID=ABC123XYZ4
ASC_ISSUER_ID=12345678-90ab-cdef-1234-567890abcdef

.p8 ファイル本体は ~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8 に置く(altool がこのパスを自動で見に行く)。

WSL2 から 1 行で TestFlight に上げる

ここまで揃えると、WSL2 側の運用は 1 行で済む。

# CFBundleVersion を bump して push したあとに
ssh {user}@{mac_addr} 'cd ~/path/to/{app_name} && bash scripts/upload.sh'

3〜6 分後に TestFlight に新ビルドが現れる。App Store Connect の Processing が完了したらすぐ自分の iPhone で Install 可能。

詰まりポイント

  • codesign が GUI プロンプトを出して固まる: build keychain の set-key-partition-list を忘れている。apple-tool:,apple:,codesign: を必ず指定
  • Auto signing が別チームの Development cert にフォールバック: Release は Manual signing 固定 + Team ID 明示が安全。ExportOptions.plistsigningStyle = manual
  • build keychain が 6 時間で自動ロックされて翌日のビルドが落ちる: archive.sh / upload.sh の冒頭で毎回 security unlock-keychain を呼ぶ。一度の set-keychain-settings -lut 21600 だけだと足りない
  • SPM の dynamic framework が Embed されない: RealmSwift のような source-distributed dynamic framework は project.pbxproj の Embed Frameworks に手動登録が必要。Firebase / GoogleMobileAds の XCFramework binary target は auto-embed されるので忘れがち
  • altool の 2 段階認証ダイアログ: パスワード認証だと 2FA でブロックされる。必ず App Store Connect API Key(.p8)認証を使う
  • Mac の sshd が起動していない: システム設定 → 一般 → 共有 → リモートログイン を有効化

よくある質問

Q. Mac は普段スリープでも大丈夫?

A. 配信するときだけ叩くなら Wake-on-LAN + システム設定の「ネットワークアクセスによりスリープ解除」を ON にすれば SSH パケットで起きる。常時稼働させるなら蓋を閉じても寝ない設定(caffeinate + 電源接続中はスリープしない)にしておく。

Q. Apple Distribution の証明書はどう発行する?

A. Mac の Xcode で Settings → Accounts → Apple Developer の Manage Certificates から「Apple Distribution」を発行 → login keychain に登録される。setup_build_keychain.sh を走らせるとそれが build keychain に複製される。

Q. Provisioning Profile を更新したらどうする?

A. Apple Developer ポータルでダウンロード → Mac で開く → Xcode の Signing & Capabilities で読み込まれているか確認。ExportOptions.plist の profile 名は変わらないので、スクリプト側の修正は不要。

Q. Mac mini を専用 archive サーバにしたい

A. M2 / M4 の Mac mini なら Xcode 26 + SPM の archive で 3〜5 分。常時電源 ON でも消費電力 10W 前後。Mac mini + LINE Notify でビルド完了通知を飛ばせば、外出中でも完走を把握できる。

Q. fastlane を使ったほうが早い?

A. fastlane は match による証明書同期や lane の構造化が便利。本記事のスクリプトは 「Mac 1 台で完結する個人配信」に絞っているので fastlane の機能のごく一部だけを薄く再実装した形。チーム開発になったら fastlane に乗り換える価値はある。

※本記事の手順は執筆時点(2026 年 5 月)の Xcode 26 / macOS 15 系で動作確認しています。Apple Distribution 証明書の発行手続きや App Store Connect の UI は変わる場合があります。動かない場合は コメント欄でお知らせください。

まとめ

Xcode を WSL2 から完全リモートで動かすのは build keychain と App Store Connect API Keyの 2 点を押さえれば素直に組める。シェルスクリプト 3 本(setup_build_keychain.sh / archive.sh / upload.sh)と ExportOptions.plist 1 枚で完結する。Mac は Xcode 専用の archive マシンとして奥に置いて、普段の開発は Linux の好みの環境で行える。

この記事が役に立ったら X(Twitter)でシェアしてもらえると喜びます。

関連記事

参考

※この記事は Claude Code を使った自動更新を試しています。

7 年放置の iOS アプリを Claude Code で復活させて App Store に出した 6 日間【Xcode 26 / Swift 6 / v3.0.0】

この記事は「7 年前に作ったきり放置している iOS アプリを最新 Xcode / Swift で動かし直してリリースしたい個人開発者」向け。自作蔵書管理アプリ「My Books」を Claude Code とペアで 6 日間モダナイズし、Xcode 26 / Swift 6 / iOS 26 対応で v3.0.0 として TestFlight 18 ビルド経由で App Store 公開(2026-05-02)するまでの全記録。コンパイル成功の後で踏んだ実機の罠も全部書く。

古い iPhone と Xcode が起動した新しい MacBook が並ぶ作業デスクの写真

この記事は何の話か

  • 2016 年初版・My Books(蔵書管理アプリ)7 年放置で塩漬けになった Swift / CocoaPods / Realm / 各種 SDK をまとめて現行スタックに置換し、2026-05-02 に App Store 公開した過程
  • WSL2 で Claude Code、ビルドは別マシンの Mac に SSH 投げ、TestFlight 配信まで 1 コマンド化
  • コンパイルが通ったあとの 実機 TestFlight クラッシュ連戦 の中身(DGCharts vtable・楽天 API 廃止・Google Books クォータ枯渇など)
  • AI ペアプロで「人間がやるべきこと」と「AI に任せられること」がどう分かれたか

数字でみるモダナイズ

項目
作業期間2026-04-25 〜 2026-05-01(6 日)
コミット数約 60
TestFlight ビルド数18
追加したテスト20(AppUtilTests / BookTests / ItemEnumsTests / ViewUtilTests)
リリースバージョンv3.0.0 / Build 18
App Store 公開2026-05-02
対象環境Xcode 26 / Swift 6 / iOS 17+ / 旧 master ブランチは凍結

全体の流れ

大きく 3 フェーズに分かれた。前半 6 日のうち 5 日がモダナイズ、半日が配信パイプ整備、残りが TestFlight 配信中の障害対応と App Store 申請。Build 18 がそのまま審査を通過して 2026-05-02 に公開された。

① モダナイズ(Phase 0–8 / 4 月 25 – 30 日)
CocoaPods → SPM、Realm @Persisted、async/await、ViewController 分割、シークレット管理、テスト追加
② TestFlight 配信パイプ構築(4 月 30 日午後)
WSL → SSH → Mac → archive → IPA → altool → TestFlight
③ TestFlight 連戦 → App Store 申請・公開(4 月 30 日夜 – 5 月 2 日)
Build 5 → 18、実機クラッシュ・API 障害を潰したあと Build 18 で審査提出 → 2026-05-02 に v3.0.0 公開

① モダナイズの中身

計画 → 削除 → API 移行 → Realm 刷新 → ViewController 分割 → シークレット整理 → テスト追加、の順。

  • CocoaPods → SPM(Realm 20.0.4 / Firebase iOS SDK 11.15.0 / Google Mobile Ads 11.13.0 / DGCharts 5.1.0)
  • Amazon PA-API 関連を全削除: 旧 RWMAmazonProductAdvertisingManager / HMAC.m / Bridging Header / BarcodeCacher 補助アプリも一掃
  • deprecated API 撲滅: arc4random_uniform / statusBarOrientation / UILocalNotification / kCLLocationAccuracyKilometer / AVCaptureConnection.videoOrientation など総 30 箇所超
  • async/await 化: ProductSearchwithTaskGroup ベースの並列に。Google Books / 楽天 / OpenBD の 3 プロバイダを async throws 統一。delegate プロトコル群を全廃
  • Realm モダナイズ: @objc dynamic@Persisted、旧マイグレーション破棄して deleteRealmIfMigrationNeeded = true + 「データをリセットしました」アラート方式に切替
  • ViewController 分割: 1135 行 → 471 行、4 つの extension(Camera / ProductSearch / Library / Speech)に責務分離
  • シークレット管理: Config/Secrets.template.xcconfig を commit、実値の Secrets.xcconfig は gitignore。Info.plist は $(VAR) 経由で展開

② TestFlight 配信パイプ

WSL2 でコード編集 → GitHub push → 別マシンの Mac に SSH → archive → IPA → TestFlight アップロードを 1 コマンドで完了させたかった。最終的にスクリプト 3 本で運用している。

scripts/setup_build_keychain.sh   # 1 度だけ実行、専用 build keychain に証明書を複製
scripts/archive.sh                # Release 構成で xcodebuild archive を非対話実行
scripts/upload.sh                 # archive 再利用で IPA → xcrun altool で TestFlight

App Store Connect API Key(.p8)認証で altool に渡す。fastlane も併用しているがテスト・ビルド検証用で、配信本体は素の xcodebuild + altool。

③ TestFlight 連戦

ここが一番時間を食った。Build 5 → 18 で 1 日半。コンパイルが通ること実機 iOS 26 で動くことの間に、思った以上に距離がある。

Build 18 を App Store 提出に回し、Apple 審査は追加質問なしで通過。2026-05-02 に v3.0.0 として App Store 公開。バージョン番号と Build 番号は TestFlight 最終ビルドのまま据え置き。

個人的に印象に残った踏み込み 5 件

1. Xcode 26 のビルドサンドボックス

死んでいた scripts/update_storyboard_strings.sh(コメント 1 行のみ)と Run Script Phase が残っていた。Xcode 26 のビルドサンドボックスは 空 Run Script Phase でもパスが解決できないとエラーに昇格させる。コメントだけ残ったスクリプトでも sandbox がエラー判定するため、Run Script Phase ごと削除する必要があった。

2. 楽天ウェブサービスが 13 日後に廃止

書影が出ない症状を切り分けていく中で、楽天ブックス API が 2026-05-14 に旧 app.rakuten.co.jp を完全停止して新 openapi.rakuten.co.jp に移行する真っ最中であることが判明。残り 13 日。

  • 必須パラメータが applicationId + accessKey に増加
  • アプリケーションタイプが Webアプリケーション(許可ウェブサイト)か バックエンドサービス(許可IP)の 2 択
  • モバイルアプリは Webアプリケーション一択。Origin ヘッダ必須、無いと 403 REQUEST_CONTEXT_BODY_HTTP_REFERRER_MISSING

新ドメインで Webアプリケーション型として再登録 → Secrets.xcconfig に新 ApplicationId / AccessKey / 許可ドメインを格納 → ProductSearchByRakuten.swiftOrigin / Referer ヘッダを毎回付与する形に書き換え。楽天ウェブサービスを使っているアプリは早めの移行を強く勧める。

3. Google Books の匿名共有クォータ枯渇

無認証で Google Books API を叩くと 429 Quota Exceeded。エラー JSON の project_number を見ると自分のものではない。仕様を読み直すと、API キー無し呼び出しは世界中の anonymous caller が同じ匿名共有プロジェクトの 20M/day クォータを取り合うとのこと。常時枯渇。自分の GCP project で API キーを発行 → ?key= クエリで送る形に修正。

4. DGCharts vtable で 4 ビルド消費 → 自前 CoreGraphics 実装に置換

Build 5–8 が同じ症状で連続クラッシュ。ShelfViewController.setChart() 内の PieChartView.data = ... setter が EXC_BAD_INSTRUCTION (brk #1).ips を atos で symbol 解決すると、DGChartsDGChartsDynamic.framework647 個の symbol が同じ stub に aliasされていた。Xcode 26 + Swift 6 + SPM の dead-strip が暴走している疑い。

静的→動的の切替も KVC 経由の objc_msgSend 逃げ道も効かず、最終的にチャート機能(3 スライス + 中央テキストのドーナツ図)を BookStatusPieView: UIView(約 60 行)として CoreGraphics で自前実装に置換した。Storyboard の customClass を差し替えて完了。

5. SSH 経由の codesign と TestFlight 配信パイプ

Mac に SSH して xcodebuild archive しても codesign が login keychain にアクセスできず失敗する問題(login keychain は SSH で unlock しても codesign から触れない仕組み)と、専用 build keychain で ACL を緩和してパイプを 1 コマンド化した話は分量が多くなったので別記事に切り出した。WSL2 → SSH → Mac → archive → IPA → altool までスクリプト 3 本で組む全手順は 「WSL2 から SSH で Mac の Xcode を archive して TestFlight に上げる最短手順」を参照。

AI ペアプロの分担

6 日通して回した結果、人間と AI がそれぞれ得意な領域がはっきりした。

担当 タスク
Claude Code crash log(.ips)の atos symbol 解決 / curl での API 切り分け / コード修正 / SSH 経由の archive・upload / docs と memory の同期更新 / コミットメッセージ生成
人間 実機操作 / App Store Connect の各種同意・申請 / GCP / 楽天 Developers のブラウザ操作 / 物理的な耳と目(音量・スピーカー挙動・バナー表示の確認)

AI に向かないタスクを無理に押しつけない切り分けが効率に直結した。逆に crash log の symbol 解決と API の curl 切り分けは AI が極端に速い

学び

  • コンパイル成功 ≠ 動作: 7 年ぶんの API 変更を Xcode が静的に拾える範囲には限界がある。実機 TestFlight に流して初めて出る不具合(DGCharts vtable / Storyboard の幽霊 outlet / ATS / ATT / AVAudioSession)が大半
  • サードパーティ API は独立進化する: Google Books の匿名共有クォータ枯渇 / 楽天の旧 API 13 日後廃止 / AdMob SDK 11+ の adSize 必須化、いずれもアプリのコードと無関係に進んでいた変更
  • テストパイプ整備が前提条件: WSL → SSH → Mac → TestFlight の 1 コマンド化が無ければ Build 1 → 18 を 1 日で回せなかった
  • Xcode 26 の罠: ENABLE_DEBUG_DYLIB / sandbox / phantom scheme / SPM Embed Frameworks。test target 立ち上げと配信パイプ構築の双方で踏む
  • 「実機で 1 日触って何も起きない」までやって完了: コード変更は前段、TestFlight crash 対応が時間的にはほぼ同等の工数になる

※本記事の作業は執筆時点(2026 年 5 月)の Xcode 26 / Swift 6 / iOS 17–26 で行った。サードパーティ API の仕様変更や SDK のバージョンによってはそのまま再現しない箇所があります。気づきがあれば コメント欄でお知らせください。

まとめ

7 年放置していた個人 iOS アプリでも、Xcode 最新版と Claude Code を組み合わせれば 6 日で v3.0.0 を App Store まで届けられる。ただし「コンパイルが通る」と「実機で動く」は別物で、TestFlight に流してからの障害対応が想像以上に時間を食う。サードパーティ API の独立進化と Xcode 26 のビルドサンドボックスは、コードに触らなくても勝手に効いてくる。配信パイプを早めに整えておくとそこから速く回せる。

App Store: My Books(蔵書管理アプリ)(v3.0.0 / 2026-05-02 公開)。役に立ったら X(Twitter)でシェアしてもらえると喜びます。

関連記事

参考

※この記事は Claude Code を使った自動更新を試しています。