CopyButton

2026年5月15日金曜日

ブログ更新を Claude Code で自動化する仕組み【テーマ選定・画像・週次 cron の試行錯誤を全部書く】

毎週コンスタントにブログ記事を出すために、テーマ選定・本文生成・画像連動・自動投稿まで Claude Code に任せる仕組みを 1 日で組み上げた。「LLM が知らない直近トレンド」をどう拾うか、「2 つの Claude Code セッションを git 経由で連携させる」とどう設計したか、「毎週金曜の cron 自動投稿」をどう安定させるか、3 つの試行錯誤の経緯と最終形を全部書く。Claude Code を使うと、これだけの仕組みが 1 日で動くところまで立ち上がる。

ノートPC、手書きメモ、マイコンボード、コーヒーカップが並ぶ作業デスクの水彩イラスト

この記事でできること

  • ブログのテーマ選定を Claude Code で自動化するアプローチがわかる
  • 画像生成のような重い処理を別の Claude Code セッションと git 連携させる方法がわかる
  • cron で headless 実行するときのハマりどころと回避策がわかる
  • 「Claude Code に任せる範囲」と「人間が判断する範囲」の線引きの考え方がわかる

使ったもの

  • Claude Code — Anthropic の CLI 版エージェント。本記事の主役
  • Blogger(投稿先)+ Blogger API v3
  • GitHub Pages — 画像配信
  • Python — テーマ偵察スクリプトと MCP サーバ
  • WSL2 + systemd cron — 毎週金曜 18:00 起動

最初にやったこと: 既存記事を Claude Code に読ませて文体を学習させる

このブログは 2015 年から書いてきて記事が約 80 本ある。新しい記事を AI に書かせるとき、これまでの文体・構成・カテゴリ傾向と揃っていないと 「いつもと別人が書いた感」が出る。検索からたどり着いた読者にも、定期読者にも違和感を与える。

そこで最初にやったのは、既存記事を Claude Code に全部読ませて、文体ガイドとトピック傾向をまとめさせること。具体的にはこうした:

  1. Blogger API(posts.list + posts.get)で全公開記事の HTML をローカルにバックアップ
  2. Claude Code に「あなたが今後この同じ著者として記事を書くために、文体の特徴・よく使う章立て・カテゴリ分布・避けるべき書き方を分析して、再利用できるようにまとめて」と依頼
  3. 出力は memory(Claude Code がプロジェクト固有の知識を持続的に保持する仕組み)に保存

抽出された知見の例:

  • 言い回しの癖: 「最短手順」「全部書く」「詰まりポイント」のような実用記事ボキャブラリ、固有のユーモアの分量感(1〜2 文に収める)
  • 章立てのよく使う形: リード → 使ったもの → 手順 → 詰まりポイント → まとめ → 関連記事
  • 14 のカテゴリ分布: 電子工作 / Raspberry Pi / Arduino / IoT / AI / 3Dプリンタ / 製品レビュー / ルンバ / ガジェット改造 / 音声合成 など
  • 避けるべき書き方: 過剰な絵文字、シリアスすぎて読み手を選ぶ語り口、「〜と思います」の連発
  • 過去 8 年分の記事タイトル全リスト: 「同じテーマを再度書かない」ための重複チェック源

これらが memory に入っていることで、Claude Code は新規記事を書くときに 「過去の自分の延長として」書ける。新規生成記事と過去記事の間の連続性が保たれる。memory はセッションをまたいで残るので、毎回プロンプトに長文の文体ガイドを貼り付ける必要がない。

なぜ自動化したのか

毎週コンスタントに記事を出したい。一方で、毎回ゼロから「最近何が話題か」を調べるのは時間が取れない。

Claude Code に「電子工作系で最近話題のテーマで書いて」と素直に頼むと、LLM の学習時点で知っているテーマから拾ってきてしまう。直近 1 年のトレンドや自分の興味とは微妙にズレる。

解決アプローチは 「直近のトレンド」「自分の興味」「過去記事との重複回避」を構造化した情報として外から流し込むこと。LLM 単体で完結させるのを諦めて、Python スクリプトで事前に下調べし、その結果を Claude Code への入力として渡す。

全体の流れ

毎週金曜 18:00 cron
BlogGen 担当 Claude Code セッション
テーマ偵察 → 記事生成 → 画像連動 → ドラフト投稿
RSS 20サイト + SNS
テーマスコアリング
別マシン Claude Code
画像生成サービス
GitHub Pages
画像配信
Blogger API でドラフト投稿
人間が中身を確認 → 公開ボタン

ハマり 1: テーマ選定 — 「LLM が知らない直近トレンド」をどう拾うか

ここで一番悩んだ。文体は memory で揃えられても、「何について書くか」を決めるのはまた別の問題。

最初の試行: WebSearch だけ → キーワードが偏る

「電子工作系で最近話題のテーマで書いて」と Claude Code に頼んで、WebSearch を数回叩いてもらう運用から始めた。やってみるとキーワードが偏る。「Raspberry Pi」「ESP32」「Home Assistant」のような LLM がよく知っている定番に収束する。直近の新製品(Pico 2 W、XIAO ESP32-S3、SCD41 など)にうまくフォーカスできず、過去にも書いたようなネタが繰り返される。

原因: WebSearch クエリ自体が LLM 知識空間内の単語に依存する。新しい単語は知らないのでクエリに含まれず、結果も新しいトレンドを拾わない、という鶏卵問題。

解決策: 構造化された「直近 1 年のトレンド」を外部から流し込む

国内 10 + 海外 10、計 20 サイトの RSS フィードを Python で定期取得して、直近 1 年分の記事タイトルからキーワードを抽出・スコアリングする research_topics.py を作った。20 サイトの内訳:

国内サイトだけだと国内ネタに偏るので、海外 10 を併用して「日本未紹介の海外先行ネタ」を拾えるようにした。海外で話題になっているのに国内サイトでは未紹介のテーマは「日本語先行紹介」枠として候補入り。

スコアリング式

各キーワードに以下のスコアを付ける:

score = frequency × (1 + cross_source × 0.4) × cross_region_bonus × affiliate_fit × novelty × sns_boost
  • frequency: 直近 1 年でそのキーワードが何記事に出てきたか
  • cross_source: 何サイトで言及されているか(広く話題なら高)
  • cross_region: 国内・海外の両方で言及されていれば +bonus(信頼性が高いトレンド)
  • affiliate_fit: Amazon で購入できる物理製品が紐づくテーマか(マイコン・センサー・部品系は高、純ソフトウェアは低)
  • novelty: 過去記事のタイトル一覧(Blogger API で取得)と照合して、すでに書いたテーマは下げる
  • sns_boost: 後述

SNS シグナル: 「自分も触れたガジェット」を 2 倍ブースト

世間で話題なテーマと、自分が SNS で言及したことがあるテーマは別物。前者だけだと「自分の興味」を反映しないし、自分が試したことがないハードを書くと内容も薄くなる。

そこで XFacebook の公式アーカイブ(個人データダウンロードで取得できる ZIP)を sns_data/ 以下に展開しておき、過去 1 年の自分の投稿テキストからキーワードを抽出。外部 RSS で言及があり、かつ自分も SNS で触れたキーワードはスコアを 2 倍ブースト

結果として上位に来るのは、「世間で盛り上がっている × 自分も触ったことがある」テーマ。経験ベースで深く書ける。

SNS アーカイブは月〜四半期に 1 回手動で再取得して上書きする運用。API スクレイピングではなく公式アーカイブを使うことで、X / Facebook の API 仕様変更で壊れないし、画像も同梱されるので記事の写真素材にも流用できる。

NG ワードフィルタ: 「ガジェットのハック」と「ライフハック」を区別する

もう一つのハマり: 「ライフハック」「生活ハック」のような汎用ワードが上位に紛れ込む。スマホアプリの組合せや時短テクなど、本ブログの読者が期待していない汎用ネタ。

これらは NG ワードとして明示的にフィルタした。「電子工作系の改造ネタ」と「単なる便利ライフハック」を区別する基準を文字列ベースで明示する必要があった。人間にとっては自明でも LLM の判断は揺れるので、ルールを文字列で固定する方が安定する。

22 種類の記事タイプ分類でローテーション

もう一段: 22 種類の「記事タイプ」分類を作った。マイコン+センサー型ばかり連発しないようにするため。例(一部):

説明
Aマイコン+センサー(Pico/ESP32 + I2C 温湿度・CO2 → MQTT 等)
B既製ガジェット改造・API ハック(SwitchBot / Echo / AirTag 等)
C3D プリンター + 機構自作
DAI × エッジ(Ollama on Pi、ローカル音声・画像認識)
Mレトロ・エミュレーション
N時計・タイムキーパー(Word clock、Nixie 時計、E-ink カレンダー)
POS 移行・デュアルブート・サーバ化(Asahi Linux、Proxmox 等)
T開発環境・IDE・ワークフロー(Node-RED、WSL2、Claude Code 等。本記事の型)
他 14 型(ロボット、無線・通信、電源、オーディオ、ストレージ、レビュー、ウェアラブル、自作 PC など)

正規表現でキーワードと型を対応付けた分類器を組んでおき、直近 5 記事でどの型が多いかresearch_topics.py 出力に含める。Claude Code は CLAUDE.md の指示で「直近 5 件のうち 2 件以上が同じ型なら、別の型を選ぶ」運用になっている。マイコン+センサーの記事を 2 週連続で出さないための仕組み。

最終的な出力フォーマット

スクリプトは毎回こんな感じの候補リストを出す(簡略例):

# 直近 5 記事の型分布: A×2, B×3 → A,B 以外を優先

# 投稿候補テーマ Top 8

1. ✨🌐💰 XIAO ESP32-S3 で CO2 モニタを ESPHome から Home Assistant に流す【型 A】
   根拠: 国内 4 サイト × 海外 6 サイト言及 / SNS で 3 回触れた / アフィリエイト適性高

2. 🌐💰 ファミコン互換機を Wipeout 移植機にする【型 M レトロ】
   根拠: 海外 5 サイト言及(国内未紹介)/ アフィリエイト適性中

3. 💰 Asahi Linux で Mac mini を Linux サーバ化する【型 P OS 移行】
   ...

(以下省略)

各候補に ✨ SNS シグナル / 🌐 国内外横断 / 💰 アフィリエイト適性のフラグが付き、根拠と該当する記事タイプも併記される。Claude Code はこの中から、直近の型分布と被らない 1〜3 件を選んで深掘り(Step 1-C のハック手法調査)に進む。

「外部から構造化情報を流し込む × LLM が選ぶ」という分業にしたことで、テーマ選定がそれなりに動くようになった。

ハマり 2: 画像をどう用意するか

記事のカバー画像(OG image)は SNS シェア時のサムネイルに使われるので、ないと CTR が大きく落ちる。LLM 単体では画像生成できないので、外部サービスとの連携が必要。

選択肢を試した: Blogger admin で手動アップロード(API 自動化と相性悪い)、xAI Grok API(月額課金)、ローカル GPU で SDXL を回す(採用)。最終的に GTX 1660 SUPER 搭載 PC に ComfyUI を立ててローカル生成、生成画像は GitHub Pages 経由で配信する構成に落ち着いた。

ローカル GPU 画像生成サーバ自体の構築過程と、参照画像から実機の見た目に寄せたり水彩イラストに変換したりする話は 前記事「ブログ用 AI 画像をローカル GPU で生成する」に詳しく書いた。本記事ではその先 — 「画像生成サービスを進化させたくなったとき、Claude Code をどう使ったか」に絞る。

ハマり 3: 2 つの Claude Code セッションを git で連携させる

これが一番面白かった。記事を書く側(BlogGen)と画像を生成する側(ImgGen)はそれぞれ 別のリポ・別のマシン・別の責務。それぞれに Claude Code セッションが付く。

でも連動はしたい。たとえば「ImgGen 側に --style illustration オプションを増やしたい」と思ったとき、その仕様は呼出側の BlogGen も知っている必要がある。

採った方法: git の仕様書ファイルを「両者が見る contract」にする

BlogGen 担当
記事生成・呼出側スクリプト・仕様書ホスト
ImgGen 担当
別マシン・画像生成サービス本体・実装側
┃ ┃
git 上の仕様書(docs/<feature>_spec.md
両者が見る contract

具体的な連携サイクル:

  1. 段階 1(仕様起票): BlogGen 側のセッションが新機能の引数名・期待動作・ワークフロー JSON 仕様を spec に書いて push
  2. 段階 2(実装): ImgGen 側のセッション(GPU PC 上)がリポを pull、実装、push back
  3. 段階 3(検証): BlogGen 側が pull、実テスト、生成画像の品質確認
  4. 段階 4(フィードバック): BlogGen 側が実測値(推奨パラメータ)を spec に追記、ステータスを ✅ に更新

良かった点:

  • 進行が止まらない: ImgGen 側の実装を待つ間、BlogGen 側は他の検証や記事生成に進められる
  • API ズレが起きにくい: spec が contract として機能するので「引数名が微妙にズレている」が起きにくい
  • 各セッションが自分のリポだけ意識すればよい: 認知負荷が低い
  • 記録が git history に残る: どのフェーズが・いつ・どちらのセッションから動いたか追跡可能

ハマりポイント:

  • 同時編集の上書き事故: 一度、BlogGen 側が spec を push する直前に ImgGen 側が別の修正を push していたのに気づかず、古い SHA で PUT して上書きしかけた → 直後の commit で復元
  • 教訓: gh CLI で spec を更新するときは PUT 直前に必ず gh api repos/.../contents/foo --jq .sha最新 SHA を取り直す
  • 引数名のリネーム時は後方互換 alias を必ず残す(呼出側が壊れる)

ハマり 4: 毎週金曜 cron + 権限ホワイトリスト

毎週金曜 18:00 JST に cron で Claude Code を --permission-mode acceptEditsheadless 実行。指定したプロンプトを 1 回実行して終了する。

最初の試行: cron 起動時に サイレントタイムアウト。原因は権限。Claude Code は対話的セッションでは「この Bash コマンド実行していい?」と都度聞くが、headless 実行ではユーザー入力ができないため、許可されない操作で詰まる。

解決策: .claude/settings.local.jsonpermissions.allow に必要な Bash パターンと WebFetch ドメインを事前に許可登録。「ホワイトリスト方式」で必要な操作を一つひとつ追加していく運用。

運用上の注意: 新しいツール呼出が必要になるたびにホワイトリストの追加が要る。cron ログ(logs/weekly_draft_*.log)を週次で眺めて「許可されなくて詰まった操作」を見つけたら settings.local に追記する。「最初に全部読んで漏れなく追加」より「動かして・壊れて・直す」のサイクルのほうが結局速かった。

例: scp の複合パターン Bash(scp take@host:* *):* がパターン途中にある形だと無効になる。Bash(scp take@host:**) のように :* を末尾に置く形に直す、というローカルなハマりも出てくる。Claude Code の /doctor コマンドで警告が出るので気付ける。

ハマり 5: Claude Code に任せる範囲 vs 人間が判断する範囲

「全部 AI に任せる」のは怖い。一方で「全部人間がやる」のは元の問題(時間が取れない)に戻る。線引きが要る。

採った設計:

フェーズ 担当
RSS / SNS からのトレンド抽出スクリプト
候補テーマからの 1 件選定Claude Code
記事 HTML 本文生成Claude Code
薬機法・景表法 NG ワードチェックClaude Code
参照画像の検索 → 取得Claude Code
AI 画像生成別セッション
GitHub Pages への画像 pushClaude Code
Blogger API でドラフト投稿Claude Code
最終的な公開ボタン人間
事実関係・タイトルのチェック人間

キーは 「ドラフトで止める」こと。Blogger API でドラフト投稿はするが、公開ボタンは押さない。人間が中身を見て、文体・事実関係・タイトルを最終チェックしてから公開する。

これにより「変な記事が出ても可逆」になる。Claude Code が試行錯誤するときの心理的安全性が確保できる。実際、テーマ選定スクリプトの初期版が「ライフハック」を量産していた時期や、画像生成パラメータ調整中も、ドラフトで止めていたおかげで安心して試せた。

よくある質問

Q. ドラフト生成に失敗したらどうなりますか?

A. cron が weekly_draft.sh を起動して Claude Code が走るが、途中で詰まったり権限が足りなかったりするとログにエラーが残ってドラフトは作られない、というだけ。次の金曜にまた走るだけなので、致命的な事故にはならない。

Q. なぜ Claude Code を 2 セッションに分けたのですか?1 セッションで両方できないのですか?

A. 物理的に別マシン(一方は GPU 搭載 PC、一方は普段使いの Linux PC)なので、それぞれの環境に張り付いた Claude Code セッションが触れる範囲が違います。別セッションで動かすことで、それぞれ自分のリポだけ意識すればよく、認知負荷が下がります。git の仕様書を contract にするとこのスタイルが成立します。

Q. 全自動公開はやらないのですか?

A. やりません。「人間が最後に見る」前提にすることで、AI による生成を試行錯誤しやすくなる、という設計です。誤情報・薬機法・景表法などのリスクを完全には自動チェックできないため、最後の関門は人間に残します。

Q. テーマ選定スクリプトはどのくらいのペースで再実行されますか?

A. cron 起動時、つまり週 1 回。RSS と SNS アーカイブから直近 1 年分のキーワード分析をその場で計算するので、結果は毎週変わります。SNS アーカイブは月〜四半期に 1 回、手動で再ダウンロードして上書きする運用です。

Q. 立ち上げにどのくらいかかりましたか?

A. 丸 1 日です。RSS 偵察スクリプト・MCP サーバ・Blogger API 連携・cron 設定・CLAUDE.md(NG ワードや文体ガイド含む)まで、CLI で対話しながら Claude Code が書き上げました。普通なら仕様策定・実装・デバッグで数週間かかる規模の仕組みが、対話するだけで一気に動くところまで進みます。

Q. 文体や記事構成は AI とどうやって揃えるのですか?

A. 過去記事を Blogger API でローカルに取得し、Claude Code に文体の特徴・章立て・カテゴリ傾向を分析させて memory に保存しました。memory はセッションをまたいで残るので、新規記事を書くたびに毎回プロンプトに長文の文体ガイドを貼り付ける必要がありません。Claude Code は「過去の自分の延長」として記事を組み立てます。

※本記事の手順は執筆時点(2026 年 5 月)で動作確認していますが、Claude Code・Blogger API・各種 OSS のバージョン更新によりそのままでは動かない場合があります。動かない場合は コメント欄でお知らせください。

まとめ

Claude Code でブログを自動化するときに効いた設計判断は次の 4 つに集約できる:

  1. 過去記事を AI に読ませて memory に文体・カテゴリ傾向を保存: 「過去の自分の延長として書く」連続性を最初に作る
  2. 「LLM 単体で完結」を諦める: 直近トレンド・自分の興味・過去記事一覧を Python で構造化して外から流し込む。LLM の知識だけで書こうとすると定番に収束する
  3. 2 つのセッションを git の仕様書で連携: 別マシン・別リポをまたぐ作業で「進行が止まらない」「API ズレが起きにくい」という効果が大きい。spec を contract にする発想は、Claude Code 以外の文脈でも応用が効く
  4. 公開のゲートは人間が押す: ドラフトで止めることで「失敗が可逆」になり、AI による生成を試行錯誤しやすくなる。完全自動化を目指さないことが結果的に自動化を進める

そして強調したいのは 立ち上げの速さ。RSS 偵察スクリプト・MCP サーバ・Blogger API 連携・cron 設定・CLAUDE.md(NG ワードや文体ガイドを含む)まで、丸 1 日で Claude Code が書き上げた。普通なら数週間かかる仕様策定・実装・デバッグが、CLI 越しに対話するだけで一気に進む。「やりたいことを言葉で説明する → ほぼ即時に動く実装が出る」サイクルが回せるので、設計判断の試行錯誤コストが劇的に下がる。

立ち上げ後は、毎週金曜の夜にドラフトが 1 本上がってきて、土日に確認して公開、というリズムになっている。完全自動化を目指さず「人間が最後に見る」前提にすることで、安心して任せる範囲を少しずつ広げられる。

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

関連記事

参考

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

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 を使った自動更新を試しています。