本文へ移動
Claude Tips

セルフホスト環境

Claude Code のクラウドセッションを自社のネットワーク内のランナーで動かす「セルフホスト環境」の仕組み・立ち上げ・本番での強化・カスタマイズ・全フラグと設定項目・セッションの身元確認をまとめます。

セルフホスト環境(self-hosted environment)は、Claude Code のクラウドセッションを自社のインフラで動かす仕組みです。claude.ai・モバイルとデスクトップのアプリ・claude --cloud・ルーティンから始めるクラウドセッションは、既定では Anthropic のインフラで動きます。これを自社のネットワークの中へ移せて、開発者の使い勝手はほぼ変わりません。

対象は、ネットワーク・ツール・コンプライアンスの都合で実行場所を自社で持ちたい管理者とプラットフォームの担当者です。

  • 多くのチームは、運用の要らない Anthropic ホストの環境で足ります
  • クラウドセッションを使わないなら、設定するものはありません(ターミナルと IDE のセッションは常に開発者のマシンで動く)
  • 常時動かす自分のマシンをほかの端末から操作したいなら、リモートコントロールを使います

要点#

  • 構成は3つ:環境(environment。claude.ai で作る名前付きの送り先)・ランナー(runner。自社のホストで動くプロセス)・セッション(開発者が始めた1つの作業)
  • 通信はすべて自社から api.anthropic.com への外向きの HTTPS。Anthropic から自社へ入ってくる接続は無い
  • 自社に残るもの:チェックアウト・ビルドの成果物・秘密・セッションが作るファイル。会話(プロンプト・応答・ツールの結果)は api.anthropic.com へ送られ、Anthropic が文字起こしを保存する。モデルへのリクエストを Bedrock か Agent Platform へ振る設定にしても、会話はセッションのイベントストリームとして api.anthropic.com へ届く
  • 制御プレーン(セッションの調整・キュー・claude.ai の画面)は Anthropic 側に残る
  • Team と Enterprise プランの公開ベータ(public beta)。既定はオフで、Owner が管理画面「Cloud environments」の「Allow self-hosted environments」をオンにする(組織でクラウドセッションが有効なことが条件)
  • ランナーは標準の claude バイナリに入っている(Claude Code v2.1.224 以降)

補足

  • Zero Data Retention を有効にした組織では使えません
  • 推論は既定で Anthropic の API を使います。ランナーを設定すると、モデルへのリクエストを自社の Amazon Bedrock か Google Cloud の Agent Platform(旧 Vertex AI)へ振れます(下の「Bedrock や Agent Platform へモデルのリクエストを送る」)。振った場合、claude.ai のサーバー管理設定と組織のポリシーはセッションに届きません
  • リポジトリは GitHub からチェックアウトします
  • Claude Security と Code Review のセッションは、まだ振られません。Claude Tag のセッションは動きますが、Access bundles はまだ使えません
  • 課金は Anthropic ホストの環境と同じく、組織の Claude Code の使用量に数えます

仕組み#

開発者がクラウドセッションを始めるとき、環境の選択に自社の環境が Anthropic ホストの環境と並んで出ます。自社の環境を選ぶと次のように進みます。

  1. 制御プレーンがセッションを環境のキューに置く
  2. ランナーがそれを受け取り、選ばれたリポジトリを clone する(git ホストへは設定した資格情報で認証する)
  3. 自社のホストで Claude Code のプロセスを起動する

ランナーは、自分で起動して常駐させても、キューに入るたびにランナーを起動するオーケストレーター(orchestrator)を別に動かしてもかまいません(ランナーは仕事が終わると自分で終了する)。環境は1回作れば、対応するすべての画面の選択に出ます。

用語 内容
環境(Environment) claude.ai の設定で作るランナーの名前付きグループ。セッションは個々のランナーではなく環境へ振られる
環境シークレット(Environment secret) ランナーが環境へ登録するための共有の資格情報(管理画面では「environment key」)。表示は作成時の1回だけで、365日後に失効する
ランナー(Runner) 自社で配備する常駐のプロセス。環境に登録してランナートークンを受け取り、セッションを poll する
セッション(Session) claude.ai・モバイルアプリ・ルーティンなどから始めた1つの作業。ランナーが起動する子の Claude Code プロセスとして動く

API のフィールド・トークンのクレーム・メトリクス名では、環境を pool、環境 ID を pool_id(ccpool_...)と書きます。CLI のフラグと環境変数は environment です(古い pool の綴りも非推奨ながら動く)。

オーナーのロック#

ランナーは一度に1人の所有者(owner)にしか仕えません。

  • 最初に受けたセッションの所有者にロックされ、以後はその所有者のセッションだけを容量まで動かす
  • ユーザーが始めたセッションの所有者は、そのユーザーのアカウント
  • Claude Tag のチャンネルのセッションはユーザーのアカウントを持たず、所有者はセッションを始めた Claude Tag のエージェント。Slack で誰が送っても所有者は同じなので、--capacity が1より大きいか --drain-grace-sec が正なら、別々の人のセッションを同じランナーが受けうる
  • 必要なランナーの最小数は、同時に動くと見込む所有者(ユーザーと Claude Tag のエージェント)の数

セッションの流れ#

  1. 空きのあるランナーがセッションを取り、リースを持つ
  2. リポジトリを作業ディレクトリへ clone し、子の Claude Code プロセスを起動する
  3. 子はイベントを HTTPS で返し、ランナーは poll を続ける。poll がリースの更新とハートビートを兼ねる
  4. ランナーの poll が止まると、リースが約60秒で切れ、サーバーは数分以内にセッションを別のランナーのキューへ戻す

poll の1回の制限時間は10秒です。タイムアウト・接続断・解釈できない応答(傍受するプロキシが自分のページを返したときなど)が起きても、動いているセッションは提供し続け、1〜2秒後に再試行します。失敗が続くたびに間隔を2倍にし(最大20秒)、リースの期限が近いときは短くします。

ランナーのライフサイクル#

ロックした所有者のために --capacity まで並行して動かし、停止の合図も退役時刻も無いあいだは、その所有者のキューから取り続けます。セッションが終わったあとは --drain-grace-sec で決まります。

  • 0(既定):アクティブなセッションが終わるとすぐ終了する。Kubernetes などが新しいディスクで再起動し、どの所有者にも仕えられる状態に戻る
  • 正の値:その秒数だけ、ロックした所有者のキューを poll してから終了する

こうして、ディスクを消さなくても所有者ごとにチェックアウトが分かれます。

--retire-at が要るかは、インフラの止め方で決まります。

  • SIGTERM で止める:不要。「シャットダウンのタイミング」のとおりドレインするか、--defer-shutdown-max-min を設定すれば持っているセッションを提供し続ける
  • 合図なしか、ドレインに足りない猶予で、決まった時刻にホストを壊す(サンドボックスの寿命の上限・スポットインスタンスの回収):その数分前の Unix 時刻を --retire-at <epoch-seconds> に渡す

退役時刻になると、ランナーは次のように動きます。

  1. 新しい仕事を取らなくなる
  2. 各セッションを --release-idle-session-min と同じ経路で解放する(次のメッセージで新しいランナーが再開する)。ターンの途中ならターンの終わりを待つ(セッションのプロセスがターンの終了を Anthropic へ報告するのを、最大 SELF_HOSTED_RUNNER_POST_TURN_SETTLE_MS まで待ってから解放する。v2.1.280 より前は、ターンが終わった時点ですぐ解放した)。ターンが終わって背景タスクが残るなら最大60秒待ち、まだ動いていても解放する。タスクが終わって結果を読むターンがまだなら、そのターンが終わるまで保つ(ターンの開始を待つのは SELF_HOSTED_RUNNER_BG_RESULT_GRACE_MS まで)
  3. すべて解放したら 0 で終了する

ホストの停止を越えて続くターンは失われます。--retire-at が無いと、合図なしの停止はクラッシュと区別できず、制御プレーンは失われたワーカーとして扱ってセッションを別のランナーへ戻します。

ネットワークの経路#

接続はすべて外向きで、Anthropic からの内向きの接続は要りません。

経路 内容
制御プレーン ランナーが api.anthropic.com を poll して仕事を取り、準備の進み具合と失敗を送る(外向きの HTTPS)
SCM コネクタ 省略可のオーケストレーターのトンネルだけが WebSocket(いまは使えない。オーケストレーターのフラグの項)
git ランナーが用意された資格情報で、HTTPS か SSH で clone と push をする(セッションごとの資格情報や、api.anthropic.com を通る Anthropic の git プロキシも選べる)
セッションの子 子の Claude Code が api.anthropic.com へのイベントストリームを保ち、推論(既定)とセッション中の git のために自分で外へつなぐ

推論は既定で Anthropic の API を使います。制御プレーンが各セッションに API のエンドポイントを渡し、セッションは Anthropic が発行するセッション単位の OAuth トークンで認証します。モデルへのリクエストを自社のクラウドアカウントへ振る方法は、「Bedrock や Agent Platform へモデルのリクエストを送る」を見てください。

  • 社内の外向きプロキシに対応する。ランナーとオーケストレーターは、各プロセスの環境の HTTPS_PROXY・NO_PROXY など(ネットワーク設定)に従う
  • 効く範囲は、制御プレーンへの呼び出し・SCM コネクタの WebSocket・HTTPS のリモートの組み込みの clone。セッションはランナーの設定を継承する
  • セッションのストリームは HTTPS 上の SSE なので、途中のプロキシに応答をバッファさせない

立ち上げる#

最小の構成は、1台のホストで1つのランナーが1つのテストセッションを動かす形です。環境の作成・状態の確認・セッションの振り分けは claude.ai で、ランナーの操作はホストのターミナルで行います。

前提#

項目 内容
組織とロール Owner が「Allow self-hosted environments」をオンにしている(オンになるまで「New」が出ない)。ロールが無ければ、持つ人に環境を作ってシークレットを渡してもらってよい。リポジトリを選べるよう、組織の GitHub との接続も要る
ランナーのホスト Linux か macOS のホストかコンテナ。api.anthropic.com・インストール用の claude.ai とリダイレクト先のダウンロードホスト・git ホストへ外向きの HTTPS が届くこと。Windows は非対応(Linux のコンテナで動かす)
時計 NTP などで同期する。ずれが5分を超えると認証に失敗する
Claude Code v2.1.224 以降(それより前は self-hosted-runner を認識しない)。latest チャンネルは公開後すぐ、stable・Homebrew の cask・安定版の apt/dnf/apk は約1週間遅れで届く
Git 2.24 以降(機能ごとの下限は「git の設定」の節)

claude self-hosted-runner --help を実行し、--environment-secret-file などの使い方が出れば準備はできています(2.1.224 より前は一般の claude --help が出るので claude update で更新する)。

環境とランナーを作る#

案内つきのセットアップを使うと、対話の Claude Code のセッションが次を進めます:管理画面での環境の作成の案内・保存したシークレットでのランナーの起動・登録の確認・./runner-setup/CHEAT-SHEET.md への早見表の書き出し。Owner のアカウントで claude auth login したマシンで動かします(API キーやサードパーティのモデルプロバイダでは使えない)。対話ができないホストでは手で行います。

bash
claude self-hosted-runner setup

手で行う手順です。

  1. 環境を作る:管理画面「Cloud environments」の「Self-hosted environments」で「New」、名前を付けて「Create」。2段目の「Copy environment key」でシークレットをコピーする(表示はこの1回だけ)。ccpool_... の環境 ID は詳細のダイアログでいつでも見られる
  2. シークレットをファイルに書く:下のコマンドのように、端末から貼り付けて Enter、続けて Ctrl+D。シェルの履歴に残らず、umask で所有者だけが読めるファイルになる。/etc/claude は root が要るので、ランナーが読める別のパスでもよい(そのときは mkdir・cat のパスと --environment-secret-file の値を揃える)
  3. ランナーを起動する:--base-dir に書き込めるか作れる絶対パスを渡す。ランナーが起動時に作り、チェックアウトとセッションごとのディレクトリを置く。省くと /workspace(すでにあって書き込めるか、root で起動したときだけ動く)。作れない・書けないと、ディレクトリを名指しするエラーで登録せずに終了する
  4. 現れたかを見る:管理画面の環境の状態が、数秒で「No runners deployed」から「Healthy」に変わる。環境を開いて「Activity」でランナー自体も見える
  5. セッションを振る:claude.ai/code で自社の環境を選んでセッションを始める。clone はホストが持つ git の資格情報で行うので、このホストが clone できるか公開のリポジトリを選ぶ。受け取ると Picked up session <session-id> がアクティブ数・容量と一緒に出る。queued のままなら下のトラブルシューティングを見る
bash
mkdir -p /etc/claude
(umask 077 && cat > /etc/claude/environment-secret)
claude self-hosted-runner --environment-secret-file '/etc/claude/environment-secret' --base-dir '<writable-dir>'

シークレットを失くした・回したいときは、環境の「Configuration」タブで新しいシークレットを作ってランナーへ配り、古いものを失効させます。失効したシークレットのランナーは次の poll で poll auth failed を記録して終了するので、オーケストレーターが新しいシークレットで起動し直します。

ランナーはアクティブなセッションが終わると終了する作りです。手元では手で再起動し、本番では終了したランナーを起動し直すオーケストレーターの下に置きます(ふつうは毎回新しいファイルシステム)。

実行中のセッションへ続きを送る#

環境で動いているセッションには、claude auth login したどのマシンの CLI からも続きを送れます(始めたマシンでなくてよい)。

bash
claude -p "your message" --cloud <session-id>
  • <session-id> には session_...・cse_... の ID か、claude.ai/code のセッションの URL を渡す
  • 成功すると Sent to cloud session. と、セッション ID と表示用のリンクが出る
  • Anthropic ホストのセッションにも同じように効く(詳しくはクラウド(Web)で使う)

本番に向けて強化する#

本番のセッションは、環境へ dispatch できる全員に代わって、モデルが指示したコードを動かします。dispatch できるのは、組織のメンバー全員と、Owner が環境へ振ったスコープで Claude Tag のチャンネルセッションを始められる人です。本番のシステムへつなぐ前に、次を済ませます。

項目 内容
セッションごとの短命なコンテナ ランナーごとに使い捨てのコンテナか VM を使い、--capacity 1 と既定の --drain-grace-sec 0 で1コンテナ1セッションにする。再起動のあいだでファイルシステムを再利用しない(「事前に温めたチェックアウト」の構成を除く)。所有者をまたいでは決して再利用しない
イメージに広い資格情報を入れない 長命の SSH キー・クラウドの資格情報・必要以上の権限の個人アクセストークンを入れない。セッション中の資格情報はラッパーからセッションごとに発行する。ラッパーより前の最初の clone には checkout フックか --use-anthropic-git-proxy を使う
環境シークレットをセッションのホストに置かない シークレットがあればランナーを登録でき、環境のどのセッションも取れる。シークレットはユーザーのコードを動かさないオーケストレーターのホストにだけ置き、1回限りのワークオーダーで動くオンデマンドのランナーを選ぶ。固定のフリートなら全セッションが読めるものとして扱い、侵害が疑われたら回す
外向きは既定で拒否 ランナーとセッションのコンテナの外向きを、自社のネットワークの境界で絞る(全環境で)
ホストの IAM は最小権限 ホストのコンピュートの ID(インスタンスプロファイル・ノードのサービスアカウント)には、ランナー自身に要る権限だけを与える。セッションはホストの資格情報を継承せず、ラッパーで自分の資格情報を得る
メタデータのエンドポイントを塞ぐ サブネット単位の外向きのポリシーではリンクローカルの通信は止まらないので、コンテナで塞ぐ(ホップ制限1の IMDSv2・GKE の Workload Identity・セッションのネットワーク名前空間で 169.254.169.254 を拒否)。ラッパーとフックにも効く。トークンの交換は、セッションの JWT で自前のトークンサービスへ認証するか、Amazon EKS の IRSA のようなファイルベースの Web ID で行う
ランナーごとのファイルシステムの分離 作業ディレクトリをほかのプロセスから読み書きできないようにする。--hooks-dir・ラッパー・ホストの ~/.claude/ はセッションから読み取り専用にする(イメージに焼くか読み取り専用でマウント)
dispatch に環境単位のアクセス制御は無い 組織のメンバーなら、どの環境へも dispatch できる。Claude Tag のチャンネルを環境へ振ると、Claude Tag のアクセス設定が許す人(既定は接続した Slack ワークスペースの全員。Claude のアカウントの有無を問わない)も始められる。ホストには、その全員が読んでよいデータと資格情報だけを置く。--lock-to-account は実行するアカウントを絞るが、dispatch できる人は絞らない。Owner は管理ページで Anthropic ホストの環境を組織全体で隠し、セルフホストだけを選択肢にできる
リポジトリ設定のガード --confine-repo-settings のモード:warn(ログに出して起動)・enforce(拒否)・off(調べない)。調べるのは、リポジトリのコミット済みの設定のうち、ワークスペースの外へ届く許可(additionalDirectories・permissions.allow の Edit/Write/NotebookEdit・sandbox.filesystem.allowWrite/allowRead)・空でない env・sandbox.enabled: false のようなオペレーターの設定の上書き。--trust-workspace にかかわらず動き、リポジトリのフック・.mcp.json・Bash のルールは対象外

注意

組織の IP 許可リストは、既定ではセルフホストのランナーの通信に効きません。ネットワークの制御として頼らず、自社の境界で外向きを既定で拒否してください。IP 許可リストを効かせたいときは、Anthropic のアカウント担当へ連絡します。

通信先の許可#

セッションのコンテナの外向きは、次のホストと、セッションが使う社内サービスだけに絞ります。

必須のホスト:

ホスト ポート 用途
api.anthropic.com 443(HTTPS。SCM コネクタだけ WSS) 制御プレーンとセッションのストリーム・推論・機能フラグ・製品の分析・JWKS の鍵・コミットの署名・--use-anthropic-git-proxy の git プロキシ・--scm-connector-host のトンネル
自社の git ホスト(github.com や GitHub Enterprise のホスト) 443 または 22 clone と push。--use-anthropic-git-proxy なら不要(git も api.anthropic.com を通る)

設定によって要るホスト:

ホスト ポート 要る場面
downloads.claude.ai 443 ネイティブインストーラで Claude Code を入れる・更新するとき(install.sh 自体は claude.ai から)。実行中は、公式の Anthropic マーケットプレイスからプラグインを入れるときだけ
storage.googleapis.com 443 /plugin に出るプラグインのインストール数とメタデータ(実行中)
code.claude.com と claude.com 443 組み込みの claude-code-guide エージェントと、事前承認された WebFetch のドキュメント参照。止めても影響はそれだけ
*.frame.claudeusercontent.com 443 組織のセッションでアーティファクトのツールが使えるときだけ。ランナーに CLAUDE_CODE_DISABLE_ARTIFACT=1 で組織の設定にかかわらず無効
registry.npmjs.org 443 プラグインを入れるとき(npm ソースの取得と依存の導入)、または npx で起動する MCP サーバーを動かすとき
http-intake.logs.us5.datadoghq.com 443 Anthropic の運用メトリクス。CLAUDE_CODE_BYOC_ENABLE_DATADOG=1 のときだけ(セルフホストでは既定オフ)
browser-intake-us5-datadoghq.com 443 Anthropic へのエラー報告。セッションのアカウントでエラー報告が有効なときだけ。DISABLE_ERROR_REPORTING=1 か DISABLE_TELEMETRY=1 で止まる
使うクラウドプロバイダのモデル用エンドポイント(bedrock-runtime.us-east-1.amazonaws.com・aiplatform.googleapis.com など。モデルのリクエスト・モデルの照会・資格情報の更新) 443 ランナーがモデルのリクエストを Amazon Bedrock か Agent Platform へ送るときだけ

許可が要らないホスト:

  • statsig.anthropic.com・*.sentry.io・claude.ai・platform.claude.com:ランナーとセッションは使わない(機能フラグは api.anthropic.com から、認証は環境シークレットで行う)
  • mcp-proxy.anthropic.com:使わない
  • ただしホスト側の作業のうち2つは claude.ai へ出るので、セッションのコンテナではなく外向きが許されたホストで行う:ワンラインのインストーラ(install.sh を claude.ai から取る)と、対話の claude auth login(案内つきのセットアップ・doctor のサインイン・CI の dispatch が使う。claude.ai・claude.com・platform.claude.com を通る)

テレメトリ:

  • セッションの子は、止めない限り運用テレメトリを Anthropic へ送る(コードとリポジトリの中身は送らない)
  • 変数はランナーのプロセスに設定する。ランナーはサーバーが渡す環境変数のあとで設定し直すので、オペレーターの値が常に勝つ
  • セルフホスト固有のものは CLAUDE_CODE_BYOC_ENABLE_DATADOG=1(Datadog の運用メトリクスへのオプトイン)
  • DISABLE_TELEMETRY・DO_NOT_TRACK・DISABLE_ERROR_REPORTING・CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC は環境変数一覧のとおり子に効く
  • DISABLE_GROWTHBOOK=1 は機能フラグの取得を止めるだけ。テレメトリも止めるには DISABLE_TELEMETRY も設定する
  • CLAUDE_CODE_ENABLE_TELEMETRY は別物で、自社のコレクターへの OpenTelemetry の出力を有効にする(利用状況の計測)

外向きのプロキシへ認証する#

接続ごとに Proxy-Authorization ヘッダーを求めるプロキシで、トークンが HTTPS_PROXY の URL に書けないほど速く変わるときに使います。HTTPS_PROXY か HTTP_PROXY にプロキシの URL を設定し、次のどちらかでヘッダーの値の出どころを指定します(どちらも v2.1.238 以降)。

  • --proxy-authorization-command <command>:その都度作るトークン向け。シェルコマンドの stdout(前後の空白を除く。Bearer <token> など)をヘッダーにする
  • --proxy-authorization-file <path>:別のプロセスが回すトークン向け。ファイルの中身(前後の空白を除く)をヘッダーにする

ランナーが起動を拒むのは次のときです。

  • 両方を設定した(片方をフラグ、もう片方を環境変数で設定しても同じ)
  • HTTPS_PROXY にも HTTP_PROXY にも http:// か https:// の URL が無い(大文字と小文字の両方を読む。ALL_PROXY は見ない)
  • オーケストレーターのサブコマンドに渡した(オーケストレーターはフラグも環境変数も受けないので、起動する各ランナーへ渡す)

仕組みと挙動:

  • ランナーは 127.0.0.1 に自前のフォワードプロキシを立て、ランナー自身・ライフサイクルフック・セッションの通信をそこへ通す。ここで Proxy-Authorization を足して自社のプロキシへ送る
  • リスナーは登録の前に起動し、起動できなければランナーは終了する
  • 設定した HTTPS_PROXY・HTTP_PROXY はリスナーを指すよう書き換えられる。接続ごとにコマンドを実行するかファイルを読み直すので、トークンの更新に再起動は要らない
  • セッションの環境では、ALL_PROXY と、設定していない綴りの HTTPS_PROXY・HTTP_PROXY を外し、NO_PROXY をランナーと同じ値に固定する
  • ヘッダーの値はログに出さない

git の設定#

ランナーはチェックアウトを管理しますが、既定では git の ID も資格情報も設定しません。方法は2つです。

方法 内容
ランナーに設定させる --configure-git(SELF_HOSTED_RUNNER_CONFIGURE_GIT=1)で、起動時にグローバルの git 設定を書かせる。ID と署名は Anthropic ホストのセッションと同じ
イメージに入れる 自社のボットの ID など、ID と push の資格情報を自分で設定する

git のバージョンの下限:

  • 2.34 以降:--configure-git の SSH のコミット署名
  • 2.32 以降:--use-anthropic-git-proxy
  • 2.29 以降:--push-outcome-on-release で push したブランチからの再開
  • 2.24:上の3つを使わず、ID も自分で管理するとき

ランナーに git を設定させる#

--configure-git で起動すると、次のグローバル設定が書かれます。

  • user.name = Claude と user.email = noreply@anthropic.com(Anthropic ホストのセッションと同じ)
  • ランナーが管理するシムで、コミットとタグに SSH 形式の署名を付ける。署名はセッション自身の資格情報で Anthropic の署名サービスが行い、Anthropic が公開する SSH 署名鍵で GitHub が検証できる
  • push.negotiate = true(push の前に、ホストにすでにあるコミットを尋ねる。v2.1.257 以降)
  • ランナーが管理するフックのディレクトリを指す core.hooksPath。commit-msg と prepare-commit-msg が、セッションの作成者の Co-authored-by: を各コミットに足す(CCR_SESSION_ACCOUNT_EMAIL から作り、未設定なら付けない)。イメージに core.hooksPath がすでにあれば、それを残してフックを入れず、[runner:git] の警告を出す

署名には git 2.34 以降が要り、古いと起動時にエラーで終了します。push の資格情報は設定しないので、イメージで用意します。

v2.1.280 以降のランナーでは、checkout と post-session のライフサイクルフックの中で作るコミットも、セッションとして署名されます(Co-authored-by: のトレーラーは付きません)。フックの中でランナーが固定する git の設定は「フックの中の git の設定」を見てください。

イメージに git の設定を入れる#

コミットには git の ID が要ります。ランナーのユーザーにかかわらず効くよう、Dockerfile でシステム全体に設定します。

dockerfile
RUN git config --system user.name "Claude" && \
    git config --system user.email "noreply@anthropic.com"

ID が無いと git commit が Please tell me who you are で失敗し、セッションが止まります。自社のボットの ID でもかまいません(ランナーは上書きしない)。

注意

長命の、または権限の広い push の資格情報を共有のイメージに焼かないでください。イメージにある資格情報は、そのイメージで動く全セッションが誰のものでも使えます。代わりに、ラッパーでセッションの JWT から作成者の ID を読み、短命で最小権限のトークンをセッションごとに発行して、セッションごとのコンテナ(--capacity 1)と組み合わせます。

イメージに資格情報を置くしかないとき(読み取り専用のデプロイキーなど)は、git ホストが許す範囲で狭く絞ります。例:1リポジトリ限定の SSH デプロイキーと url.<base>.insteadOf・最小権限のトークンを返す credential.helper・狭いキーを指す GIT_SSH_COMMAND。

どの仕組みもプロンプト無しで動く必要があります。組み込みの clone と fetch は次のようにプロンプトを止めるためです。

  • GIT_TERMINAL_PROMPT=0:ユーザー名とパスワードを尋ねない
  • SSH を BatchMode=yes で動かす(GIT_SSH_COMMAND があればそこへ足す):パスフレーズやホストの確認を尋ねない
  • GCM_INTERACTIVE=never:Git Credential Manager がサインインのダイアログを開かない
  • core.askPass を空にする:askpass のヘルパーは環境変数 GIT_ASKPASS で設定する

あわせて知っておくこと:

  • 資格情報が拒否されるか無いと、数回再試行したあと、結果を push するリポジトリなら準備を失敗させる(読むだけのリポジトリは飛ばすことがある。トラブルシューティングの表)
  • これらの設定はセッションの環境へ渡らない
  • GIT_SSH_COMMAND・GIT_ASKPASS が指すプログラムと、そのキーやファイルは、セッションが書き込めない場所に置く(ランナー自身の git が実行するため)
  • チェックアウトのディレクトリがランナーと別の uid の所有だと git が拒むので、git config --system --add safe.directory '*' を足す

Anthropic の git プロキシを使う#

--use-anthropic-git-proxy(CLAUDE_RUNNER_USE_GIT_PROXY=1)で起動すると、セッション自身の短命なトークンで認証する Anthropic の git プロキシ経由で clone します。Anthropic ホストの環境と同じ認証経路で、イメージに git の資格情報(SSH キー・credential helper・.netrc)は要りません。

  • プロキシが使うトークン:通常のユーザーのセッションは作成者が保存した GitHub か GitHub Enterprise の OAuth トークン、ボットとエージェントのセッションは組織の GitHub App のインストールのトークン
  • --capacity 1 と git 2.32 以降が要る(プロキシの URL がセッション単位のため。古い git はセッションを分ける設定を無視する)。満たさないと起動を拒否する
  • git ホストが Anthropic のインフラから届く必要がある。社内からしか届かないホストには、代わりに checkout フックを使う
  • 1ランナー1セッションなので、並行にはレプリカを増やす
  • 有効だと --git-host-rewrite と --git-ssh-rewrite は効かない(プロキシの URL は api.anthropic.com を指す)
  • 登録時にオプトインを Anthropic へ伝え、起動時に Registering as opted in to Anthropic-managed git (--use-anthropic-git-proxy) を出す(v2.1.267 以降。それより前はフラグを受けるだけ)。セッションがセッション単位のプロキシ URL を使うと [runner:warn] が1行出る

注意

このページの Kubernetes と Docker Compose の例は --capacity 4 です。そのまま --use-anthropic-git-proxy を足すと、再起動のたびに起動時に終了します。--capacity 1 にして、並行にはレプリカを増やします。

GitHub CLI なしで GitHub の API を使う#

イメージに GitHub CLI(gh)が無くても、Claude Code が組み込みの gh を出せるので、Claude はプルリクエストを開き、コメントし、CI の結果を読めます。組み込みの gh は、Anthropic が管理する git を使うランナー向けで、使えるのは GitHub の REST API を呼ぶ gh api だけです。ランナーのイメージの Claude Code が v2.1.287 以降のときに使えます。

gh pr create の代わりに、次のようにプルリクエストを開きます。組み込みの gh が、いまのリポジトリの {owner} と {repo} を埋めます。

bash
gh api repos/{owner}/{repo}/pulls -f title='Fix' -f head='my-branch' -f base='main'
  • 資格情報:REST のリクエストは Anthropic が管理する git を通り、GitHub の資格情報は Anthropic 側が与える。イメージに GitHub のトークンは要らない
  • 使えるセッション:Anthropic がセッションごとに、Anthropic が管理する git でそのセッションの gh を賄うかを決める。賄うときは、ランナーがセッションについて出す [runner:session] governed git ACTIVE の行に gh_path_shim=true が出る。賄わないセッションには gh が無い
  • jq:--jq を使うならイメージに jq を入れる
  • CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:セッションの環境にこれがあると、組み込みの gh は出ず、セッションに gh が無い

イメージに GitHub CLI があるときは、セッションはそちらを使います。

Anthropic が管理する git で私設の認証局を信頼させる#

TLS を検査するプロキシが署名する私設の CA を、git に信頼させる方法です(ランナーが Claude Code v2.1.283 以降のとき)。

  • システムの証明書ストアに CA を入れる:どちらの変数も要らない
  • GIT_SSL_CAINFO:自社の CA の PEM ファイルを指す(例 /etc/ssl/corp-ca.pem)
  • GIT_SSL_NO_VERIFY:再署名するプロキシの後ろでは役に立たない。ランナー自身の Anthropic が管理する git 経由の clone は証明書を確かめるので、上の2つで CA を信頼させるまで失敗する

セッションのトークンを Anthropic が管理する git へ運ぶ接続での、2つの変数の扱いです(command フックはセッションの環境で始まるので、セッション内の git と同じ)。

変数 git が動く場所 扱い
GIT_SSL_CAINFO ランナー自身の clone と fetch 変数なしで動き、ランナーが書くセッション単位の証明書ファイル(ホストの CA バンドル+自社の証明書)で確かめる
GIT_SSL_CAINFO セッション内の git 自社のファイルを指す http.sslCAInfo と、Anthropic の git をセッション単位のファイルで確かめる http.<url>.sslCAInfo を受ける
GIT_SSL_CAINFO checkout・post-session のフック 変数をそのまま継承する
GIT_SSL_NO_VERIFY ランナー自身の clone と fetch 変数なしで動き、証明書を確かめる
GIT_SSL_NO_VERIFY セッション内の git http.sslVerify=false を受ける。Anthropic の git だけは http.<url>.sslVerify=true で確認を保つ
GIT_SSL_NO_VERIFY checkout・post-session のフック セッションが Anthropic の git のリポジトリを持つときは http.sslVerify=false を受ける

私設ネットワーク向けに git の URL を書き換える#

リポジトリの URL は、git ホストのホスト名の HTTPS で届きます(GitHub Enterprise では、claude.ai の管理設定の GitHub Enterprise の連携に設定したホスト名)。clone の前に、繰り返し指定できる2つのフラグで書き換えられます。

  • --git-host-rewrite <from>=<to>:split-horizon の DNS 向け(Anthropic は外部のホスト名で、ランナーは内部のホスト名で届く)
  • --git-ssh-rewrite <host>:SSH しか受けないホスト向け。https://<host>/owner/repo を git@<host>:owner/repo にする

ホストの書き換えが先に走るので、両方使うなら --git-ssh-rewrite には内部のホスト名を書きます。checkout を丸ごと自分で行うなら checkout フックを使います。

ランナーのイメージと配備#

Anthropic はビルド済みのランナーのイメージを出していません。claude のバイナリに、リポジトリが要るツールチェーン(言語のランタイム・コンパイラ・パッケージマネージャー・MCP のサイドカー)を重ねて自分で作ります。

補足

このページのレシピは --capacity 4 で、1コンテナが同じ所有者の最大4セッションを並行して動かします。これは強化の項の「セッションごとのコンテナ」になっていません。本番のシステムへつなぐ前に、--capacity 1 にするか、環境シークレットもセッションのホストから離せるオンデマンドのランナーを使います。

最小の Dockerfile です。

dockerfile
FROM debian:bookworm-slim
ARG CLAUDE_CODE_VERSION
RUN apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates openssh-client \
 && rm -rf /var/lib/apt/lists/*
RUN curl -fsSL "https://downloads.claude.ai/claude-code-releases/${CLAUDE_CODE_VERSION:?set with --build-arg CLAUDE_CODE_VERSION}/linux-x64/claude" \
      -o /usr/local/bin/claude && chmod +x /usr/local/bin/claude
RUN git config --system user.name "Claude" \
 && git config --system user.email "noreply@anthropic.com" \
 && git config --system --add safe.directory '*'
ENTRYPOINT ["claude"]
  • ARM のノードは linux-arm64、Alpine など musl ベースは linux-x64-musl か linux-arm64-musl
  • URL は標準のリリースの置き場所なので、署名つきのマニフェストでバイナリを検証できる
  • ランナーには v2.1.224 以降が要る

ビルドして自社のレジストリへ push します。

bash
docker build \
  --build-arg CLAUDE_CODE_VERSION="$(curl -fsSL https://downloads.claude.ai/claude-code-releases/stable)" \
  -t <your-registry>/claude-runner:latest .
  • コマンド置換でその時点の stable の版を引くので、新しい stable が出たあとに同じコマンドを打つと作り直される
  • 再現できるビルドにするなら、版を CLAUDE_CODE_VERSION に直接渡す
  • 新しいモデルが stable より新しい版を求めるときは、URL の stable を latest にする

CPU とメモリの見積もり#

大きさは、ランナーのプロセスではなくセッション(Claude Code と、それが動かすビルド・テスト・パッケージの導入・MCP サーバー)に合わせます。1セッションの出発点です。

項目 値
メモリ リクエストも上限も 4 GiB(Claude Code の最小の 4 GB を満たす)。同じ値にしてスケジューラーに全体を勘定させる。上限に達するとカーネルがプロセスを kill し、セッションが途中で終わりうる
CPU リクエスト 2 CPU、上限 4 CPU(ビルド中に伸ばせる)。上限では絞られるだけで kill されない
yaml
resources:
  requests:
    cpu: "2"
    memory: 4Gi
  limits:
    cpu: "4"
    memory: 4Gi
  • 負荷の山はビルドとテストなので、代表的なビルドでピークを測り、その上に Claude Code を載せる余地が無ければ値を上げる
  • --capacity は同時のセッション数を絞るだけで、CPU とメモリは分けない。1セッションの取り分を絞るならラッパーから制限を掛ける
  • --capacity 1(またはオンデマンドのランナーで spawn-runner フックが出すワークロード)なら1セッション分の値、容量が1より大きいならそれに容量を掛けた値にする
  • Kubernetes と Docker Compose のレシピは上限なしの --capacity 4 なので、上限を足す

Kubernetes#

ランナーは既定でポート 8080 の GET /healthz を出すので(--health-port で変更)、プローブは追加の設定なしで動きます。

  • /healthz はプロセスが生きていれば 200 を返すので、分かるのは死んだプロセスだけ。poll が止まったランナーは /metrics の last_poll_age_seconds で監視する
  • 下の Deployment は、環境シークレットを Secret からマウントし、liveness と readiness を /healthz に向け、終了猶予を90秒にしている(理由は「シャットダウンのタイミング」)
  • CPU とメモリの resources は入れていないので、容量に合わせて足す
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: claude-runner
  namespace: claude-runners
spec:
  replicas: 3
  selector:
    matchLabels:
      app: claude-runner
  template:
    metadata:
      labels:
        app: claude-runner
        app.kubernetes.io/part-of: claude-code-self-hosted-runner
    spec:
      terminationGracePeriodSeconds: 90
      containers:
        - name: runner
          image: <your-registry>/claude-runner:latest
          args:
            - self-hosted-runner
            - --environment-secret-file
            - /etc/claude/environment-secret
            - --capacity
            - "4"
          volumeMounts:
            - name: environment-secret
              mountPath: /etc/claude
              readOnly: true
          ports:
            - name: health
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 30
      volumes:
        - name: environment-secret
          secret:
            secretName: claude-runner-environment-secret

先に名前空間を作り、Secret を作ります。元にするのは、「Copy environment key」の値を (umask 077 && cat > ./environment-secret) で書いたローカルのファイル(貼って Enter、続けて Ctrl+D)で、作ったあとは消します。

bash
kubectl create namespace claude-runners
kubectl create secret generic claude-runner-environment-secret -n claude-runners --from-file=environment-secret=./environment-secret

Docker Compose#

次のサービスは、ランナーが終了するたびに(クラッシュでもドレイン後の通常の終了でも)再起動します。

  • 評価向け。Docker の再起動は書き込み層を残したまま同じコンテナを動かすので、強化の項の「新しいファイルシステム」にならない
  • 本番では、実行ごとにコンテナを作り直すか、それをするオーケストレーターを使う
  • 終了を繰り返すコンテナには、Docker が再起動までの待ちを上限まで延ばす
yaml
services:
  claude-runner:
    image: <your-registry>/claude-runner:latest
    command:
      - self-hosted-runner
      - --environment-secret-file
      - /run/secrets/environment-secret
      - --capacity
      - "4"
    secrets:
      - environment-secret
    restart: always
    stop_grace_period: 90s

secrets:
  environment-secret:
    file: ./environment-secret

シャットダウンのタイミング#

SIGTERM を受けたランナーの動きです(--defer-shutdown-max-min が無いとき)。

  1. 新しい仕事を取らなくなる
  2. 進行中のターンが終わるのを最大 --drain-wait-sec(既定0)待つ
  3. 各セッションのプロセスツリー(Claude が走らせていたコマンドを含む)を終了する
  4. post-session フックを実行する

ドレイン全体の最大時間:

  • --session-stop-grace-sec + --drain-wait-sec + --post-session-hook-timeout-sec + 後始末の固定15秒(--push-outcome-on-release があればさらに30秒)
  • 既定で80秒。ランナーが起動時に合計をログに出す
  • セッションはこの1つの予算の中で並行してドレインするので、--capacity を上げても増えない

--drain-wait-sec の考え方:

  • 既定の0では、ローリングの再起動が進行中のターンを切り、セッションは別のランナーで再開して push していない作業を失う
  • 値を設定し、猶予も合わせて延ばせば、ターンを終えてから止まれる
  • ドレインのあいだランナーは容量0でハートビートを送り続けるので、post-session フックの途中でリースが切れて別のランナーへ戻ることはない(ハートビートは登録を外す直前に止まる)

ホストには、起動時のログに出る合計時間を与えます。止め方ごとの設定です。

止め方 設定
SIGTERM の猶予期間がある Kubernetes の terminationGracePeriodSeconds・Docker Compose の stop_grace_period などを合計以上にする。Kubernetes の既定の30秒では、ドレインの途中で Pod が止まる
--retire-at を使う 退役時刻からホストの停止までの余裕を、典型的なターン・背景タスクの保持・同じ合計をまかなえる長さにする。退役時刻は起動のたびに計算する(date +%s に寿命を足すなど)
--defer-shutdown-max-min を使う ドレインの合計に、設定した分数と解放後の猶予(既定75秒)を足す。合わせた値もランナーが起動時に出す

最初の合図を過ぎてドレインを遅らせる#

--defer-shutdown-max-min <n> を設定すると(v2.1.238 以降)、最初の SIGTERM か SIGINT ですぐドレインせず、持っているセッションを最大 n 分提供し続けます。合図のあとは新しい仕事を取らず、制御プレーンにセッションを戻されないよう poll だけ続けます。合図から数えて3つの段階があります。

段階 動き
最初の n 分 通常どおり提供し、--startup-timeout-min と --kill-session-after-min も効く。--release-idle-session-min があれば、その時間アイドルのセッションを解放する(無ければ残る)
n 分が尽きたとき 持っているセッションをアイドルかどうかにかかわらず解放する。ターンの途中ならその終わりを待ち、背景タスクには最大60秒を足して待つ
解放後の猶予が尽きたとき 残ったセッションをドレインし、制御プレーンがすぐ別のランナーへ戻す。猶予は n 分が尽きたときに始まり、既定75秒(--drain-wait-sec が60秒を超えるなら --drain-wait-sec + 15秒)
  • どの段階でも、セッションが0になればすぐ 0 で終了する
  • 2回目の合図で段階を打ち切ってすぐドレインし、ドレイン開始後の次の合図で強制終了する
  • ホストの停止のタイムアウトは、n 分+解放後の猶予+ドレイン全体以上にする(既定では n 分+155秒。起動時に合計が出る)
  • 停止のタイムアウトが先に尽きるとホストがランナーを kill し、post-session フックは走らず、制御プレーンが数分以内にセッションを戻す
  • その時間を与えられないなら、フラグを設定せず最初の合図でドレインさせる

実行中の post-session フックに届くもの#

post-session フックとセッションの子は、ランナーとは別の POSIX のプロセスグループで動くので、止め方によって届き方が違います。

止め方 届き方
ドレイン中のランナーへの SIGTERM(--defer-shutdown-max-min なしでは2回目) ランナーがすぐ強制終了し、ドレインの残りは飛ぶ。フックには合図が届かない。init が孤児を引き取る素のホストではフックは最後まで走るが、監視もタイムアウトも外れ、閉じたログのパイプへ書くと SIGPIPE で死にうる(生き残らせたいフックは出力をファイルへ向ける)。このページのコンテナのレシピ(ランナーが PID 1 で、終了がコンテナを止める)と systemd の既定の KillMode=control-group では、フックも死ぬと考えて猶予期間に頼る
プロセスグループへの合図(kill -- -<pid>・シェルのジョブ制御・グループ単位のウォッチドッグ) ランナーと実行中の checkout フックには届く。post-session フックとセッションの子には届かない
cgroup ごとの kill(systemd の既定の KillMode=control-group、terminationGracePeriodSeconds 切れの SIGKILL) フックも含めて全部に届く。猶予期間でドレイン全体をまかなう
フック自身のタイムアウト --post-session-hook-timeout-sec を超えると、フックのプロセスグループに SIGTERM、2秒後に SIGKILL(フックが fork した tar・rsync・git も孤児にならず一緒に終わる)。出力をファイルへ向けて SIGTERM の段を生き延びた子には、ランナーの手が届かない

ドレインの開始時と強制終了時に、ランナーはまだ動いている post-session フックの数をログに出します。何もせずに終わったドレインか、スナップショットの途中だったかを見分けられます。

ベースのディレクトリと容量をランナー間で揃える#

ランナーがセッションの途中で死ぬと、セッションはキューへ戻り、環境の別のランナーが拾います。

  • チェックアウトのパスは --base-dir と --capacity から決まる(--capacity 1 は --base-dir の直下、1より大きいとセッションごとの worktree)
  • どちらかがランナー間で違うと、再開したセッションの作業ディレクトリが変わり、エージェントが覚えていた絶対パスが無い場所を指す
  • 環境の全ランナーで同じ値にする。インスタンス ID やホスト名のようなホストごとの値を使わない

ベースのディレクトリの既定は /workspace で、ランナーに書き込み権限が要ります。

  • 登録の前にディレクトリを作って書けるかを確かめ、だめなら cannot create or write to base directory で終了する
  • root で動くランナーは自分で作る
  • root でないなら、起動前に作ってランナーのユーザーの所有にするか、そのユーザーのディレクトリを --base-dir に渡す

事前に温めたチェックアウトを再利用する#

大きなリポジトリでは clone が起動時間の大半を占めることがあります。--capacity 1 で checkout フックが無いとき、ランナーはリポジトリごとに1つの正準な clone を <base-dir>/<repo-owner>/<repo> に置いて使い回します(求められた ref を fetch し、HEAD を切り離し、ハードリセットする)。冷えた clone を避ける方法は2つです。

  • イメージに作り込む:そのパスに clone を入れておく。新しいコンテナがディスクを使い回さずに温まった clone から始まる
  • 永続ボリュームに置く:--lock-to-account で1アカウントに先にロックしたランナーで、--base-dir を永続ボリュームに向ける(ディスクがそのアカウント専用になる)。先にロックしたランナーは Claude Tag のチャンネルセッションを取らないので、それを受けるランナーには使えない

使い回すときの決まりごと:

  • clone の形は問わない:完全・浅い・単一ブランチのどれでもそのまま使う。既存の clone の fetch には --depth を渡さない。CLAUDE_RUNNER_FETCH_DEPTH(full・0・数値。既定50)が効くのは、clone がまだ無いときの最初の clone だけ
  • 追跡しているファイルはリセットされ、追跡外は残る:各セッションはハードリセットから始まるが git clean はしないので、同じ所有者の前のセッションの追跡外のファイルが残る
  • セッションごとのディレクトリも残る:<base-dir>/_sessions/ に、セッションの Claude の設定ディレクトリ(会話の文字起こしのコピーを含む)・アップロードされたファイル・セッションのディレクトリ(worktree と checkout フックのチェックアウトを含む)ができる。既定では終了後も残り、全セッションがランナーと同じユーザーで動くので、後のセッションが読める。永続の --base-dir(や同じファイルシステムでの再起動)では、増える分を見込んでボリュームを取る
  • --remove-session-state で消せる:セッションの終了ごとにセッションごとのディレクトリを消す(ベストエフォート。消す前に kill されると残る。正準な clone と、一時ディレクトリなどセッションが書いたほかのファイルは残る)
  • git プロキシではリセットがチェックアウトになる:各セッションの前に clone の .git/ を整え(オブジェクト・ref・浅い状態は保ち、インデックスは消す)、作業ツリー全体をチェックアウトし直す(再 clone はしない)。サブモジュールの事前の clone には対応しない
  • 遅い clone に工夫は要らない:git の各操作は「進捗なし120秒」のウォッチドッグと30分の上限で縛られるので、進捗を出し続ける clone は最後まで終わる

バージョンを固定する#

セッションの子は、ランナー自身のバイナリで動き、子の中の自動更新は止められます。全セッションがホストかイメージに入れた版で動き、ホストを更新したら次のランナーの起動から効きます。

セッションのモデルが、動かしている版より新しい Claude Code を求めることがあります。そのモデルへのリクエストは Claude Code does not support this model で拒否されます(エラー一覧)。固定する前に、使うモデルが求める版を確かめます(モデル)。

  • フリートを1つの版に揃える:版を固定してイメージを作るか、素のホストなら特定の版を入れて自動更新を止める
  • 更新する:新しい版を入れるかイメージを作り直し、ランナーを再起動する
  • プラグイン:マーケットプレイスも自動更新されない。バイナリを固定したままプラグインだけ更新させるなら、ランナーの環境に FORCE_AUTOUPDATE_PLUGINS=1

フリートを拡縮する#

ランナーの増減はオーケストレーターの役目です。所有者のロックがあるので、レプリカの最小数は同時に動くと見込むユーザーと Claude Tag のエージェントの数です(--capacity が決めるのは1人の所有者の並行数)。

  • 固定のフリート:決まった数のレプリカを動かし、各ランナーの Prometheus のメトリクスで増減する
  • オンデマンドのランナー:claude self-hosted-runner orchestrator を動かす。ランナーの無いまま待っているセッションを Anthropic から poll し、spawn-runner フックでセッションごとに1つ起動する

既知の問題と制限#

  • コネクタの通信は自社の外を通る
    • GitHub・Slack・Linear などの claude.ai のコネクタのツールは、ランナーではなく Anthropic のインフラから api.anthropic.com 経由で呼ばれる
    • 外すには allowedMcpServers・deniedMcpServers のポリシーで絞る。Anthropic が配るコネクタ・ランナーが seed するサーバー・ユーザーが足すサーバーのすべてに効くので、ほかのサーバー向けの許可リストを配ると、配られたコネクタもブロックされる
    • URL の許可リストと併せてコネクタを使うなら、https://api.anthropic.com/v2/ccr-sessions/*・https://api.anthropic.com/v1/code/sessions/*・https://api.anthropic.com/v1/code/mcp/* に一致する項目を足す
    • 通信を社内に留めるなら、同等のツールをイメージのローカルの MCP サーバーとして動かす
  • アイドルと数えられないセッションがある
    • 終わらない背景タスクを持つセッションと、実行中のツール呼び出しから承認を待つセッションは、--release-idle-session-min で解放されない
    • 枠を占め続けないよう、必ず --kill-session-after-min を上限として併せて設定する(最長のセッションより上。8時間なら 480)
    • v2.1.260 以降は、上限に達してもすぐ終了せず猶予(既定15分。SELF_HOSTED_RUNNER_MAX_LIFETIME_GRACE_MS)を与える。ユーザー待ちならすぐ、背景タスクだけなら最大60秒待って、ターンが動いていればそれが終わるかユーザー待ちになってから解放する(次のメッセージで再開)
    • 猶予が尽きてもランナーに残っていれば終了し、動いていたターンの作業は失われる。解放されたセッションは新しい clone から再開するので、push していない作業はどちらでも失われる
    • v2.1.260 より前は上限で終了した
    • 会話がアイドルになったセッションの枠を空けるのは --release-idle-session-min の役目
  • 再開したセッションは push していない作業を失う:新しいランナーが開始ブランチから clone し直す
    • コミット済みの作業を残すには --push-outcome-on-release。解放の前に結果のブランチをベストエフォートで push し、再開時はそのコミットから始まる。未コミットの変更は失われる
    • 有効にする前に、リモートの claude/* への push を制限する(ブランチのルールセットなど)。再開時、ランナーは前に push したブランチを、誰が push したか確かめずに fetch する
    • 再開時はセッションごとの設定ディレクトリとシェルの状態も捨てる
  • 途中で足したリポジトリは clone に失敗することがある:Claude が HTTPS の git clone で取る。--use-anthropic-git-proxy を使わないランナーでは、ホストにそのリポジトリを読める資格情報が無いと git の認証エラーで失敗する。できるなら要るリポジトリは作成時にすべて選ぶ
  • 一部のコネクタが出ない:claude.ai の設定でまだ接続していないコネクタは一覧に出ず、接続も促されない。先に設定で接続してから新しいセッションを始める(実行中のセッションに足してもツールは使えない)

問題は Anthropic のアカウント担当へ連絡します。

トラブルシューティング#

ランナーのホストで doctor を実行すると、ランナーのログと状態を持った対話の Claude Code のセッションが案内つきで診断します。先にそのホストで claude auth login しておくと、環境・ランナー・キューまで調べられます(しないと、ローカルのヘルスエンドポイント・メトリクス・ランナーのログだけ。ログは --log-file で起動したときだけ読める)。

bash
claude self-hosted-runner doctor
症状 原因と対処
ランナーが環境に現れない api.anthropic.com へ HTTPS で届くか・環境シークレットが最新か・時計のずれが5分以内かを見る。認証の失敗なら拒否の理由つきの [runner:fatal] が出る
cannot create or write to base directory で起動時に終了 --base-dir(既定 /workspace)を作れない・書けない。所有者を直すか、書けるパスを渡す。確認がタイムアウトした [runner:fatal] なら、NFS か CSI のマウントが固まっているのでマウントを調べる。どちらも --log-file を開く前に stderr へ出るので、ターミナルかコンテナのログで探す(v2.1.225 より前は起動時に確かめず、セッションを拾ってから失敗した)
セッションが queued のまま ランナーがみな別の所有者にロックされている可能性。claude_code_self_hosted_runner_locked_account のメトリクスか [runner:health] の locked_account で持ち主を見る(メールは act.email を持つトークンが出たあとだけ。Claude Tag のエージェントでは付かず locked_account=yes)。レプリカを足すか、ドレインと再起動を待つ。オンデマンドならオーケストレーターを調べる
拾われた直後に失敗する claude.ai/code でセッションを開いてエラーを見る。多いのは、イメージに git の資格情報やビルドツールが無いこと
認証つきのプロキシ越しに外へ届かない --proxy-authorization-command・--proxy-authorization-file の取得が失敗・30秒でタイムアウト・空だと、その接続に 502 Bad Gateway を返して理由をログに出す(コマンドの stderr とヘッダーの値は出さない)。ホストでコマンドを打ち、ヘッダーの値が丸ごと stdout に出るかを見る。could not start the proxy-authorization listener で終了するなら、ループバックのリスナーを開けなかった
rejecting the malformed poll response を含む Poll failed poll の応答が期待した JSON でない。多くは、途中の傍受するプロキシかキャプティブポータルが自分のページを返している。ランナーは応答を捨てて claude_code_self_hosted_runner_poll_errors_total の transport に数え、失敗時の間隔で再試行し、動いているセッションは提供し続ける。プロキシが api.anthropic.com の応答を変えずに通すようにする(v2.1.246 より前は空のキューと読み、セッションやランナーを終わらせることがあった)
セッションのブランチがリモートに無い 読むだけのソースは飛ばして続ける。結果を push するソースでブランチが消えている(マージ後の自動削除など)と、リポジトリとブランチを名指しし、戻して再試行するよう求めるエラーで失敗する。飛ばしてリポジトリが0になるときも同じ(v2.1.228 より前は空のディレクトリで始まった)
リポジトリの1つが欠けたまま始まる checkout フックの無いランナーで、読むだけのリポジトリへのアクセスを git ホストがはっきり拒むと(リポジトリが無いと答える・資格情報が見つからない・認証に失敗する)、[runner:warn] could not access context source を出して残りで始める。ネットワークの失敗・タイムアウト・HTTP 403・結果を push するリポジトリの拒否は失敗にする。0になるときも失敗。--use-anthropic-git-proxy ではプロキシ自身が拒んだものだけ飛ばす。確認は開始のたびに走るので、権限が付けば次から clone される(v2.1.274 より前は拒否で失敗した)
起動に数分かかる たいてい最初の clone。claude_code_self_hosted_runner_session_init_duration_seconds で確かめ、事前に温めたチェックアウトか小さい CLAUDE_RUNNER_FETCH_DEPTH で減らす
ターンが 401 で失敗する セッションは、ランナーが Anthropic から取って stdin で回す短命な CLAUDE_CODE_OAUTH_TOKEN で推論する。401 か 403 で終わると新しいトークンを取って渡す(失敗したターンは再試行しない)。取得に失敗すると inference_token refresh failed と次の再試行の時期を出し、セッションのあいだ再試行し続ける。約30分で全呼び出しが失敗し始めるなら、ラッパーが stdin を切っている(「stdin とファイルディスクリプタ 3 をつないだままにする」)。v2.1.274 より前は数回で諦め、次の予定の取得まで 401 が続いた
Pod がドレインの途中で kill される terminationGracePeriodSeconds を、起動時のログに出る値以上にする

ランナーのログの出どころ:

  • ログの初期化のあと、ライフサイクルのログ([runner:fatal] を含む)は stdout、デバッグの出力は stderr に、JSON ではない平文の行で出る。上の起動時の失敗はそれより前に stderr へ出る
  • 両方を --log-file(doctor が tail できる)かプラットフォームのログ収集で取る
  • セッションの子は別のデバッグログを書き、失敗するとランナーがその末尾を claude.ai/code のセッションに添える
  • --remove-session-state でなければ、失敗したセッションのログをディスクにも残し、パスをランナーのログに出す

ランナーが終了したとき#

オンデマンドのランナーは、ワークオーダーが1回限りなので再起動しません。固定のフリートでは、終わり方で扱いを分けます。

  • 通常の終了:セッションを終えてドレインした・退役時刻に達した・止めるよう言われた。容量を戻すため再起動する
  • 起動の失敗:設定かホストの問題で数秒で終了し、再起動しても毎回同じ。速く再起動しても無駄なので、出力を読んで直す

スーパーバイザーは、終了のたびに再起動し、起動直後の終了が続くなら待ちを延ばし、それでも続くなら人に知らせるようにします。

起動の失敗を見分ける#

起動できないとき、ランナーは理由の行を出して終了します。多くは [runner:fatal] を含み、一部(フラグを解析できない・環境シークレットを読めない・ベースのディレクトリを作れない/書けない)は error: で始まって次の行が --help を指します。

text
[runner:fatal] --use-anthropic-git-proxy requires --capacity 1 (the proxy URL is per-session and linked worktrees share origin). Omit --use-anthropic-git-proxy or set --capacity 1.
  • 探す場所:stdout と stderr・コンテナのログ・--log-file(error: の行はログのファイルを開く前に出るので、ターミナルかコンテナのログ)
  • 行がまったく無い:ホストに kill されたランナーは何も出さない。メモリの上限などでホストかオーケストレーターが止めていないかを見る
  • 終了コード:繰り返すエラー用のコードは分けていない。設定の誤り(フラグの組み合わせなど)も、自然に直りうる失敗(API に届かないなど)も同じコード。待ちを延ばすかは終了までの速さで決め、理由は出力で読む
  • 健全に見える環境:--configure-git と Anthropic の git プロキシの資格情報の設定は登録のあとに走る。ここで失敗すると、プロセスが終わったあとも環境がランナーを数分表示し、誰も仕事を拾っていないのに「Healthy」と見えることがある。健全なのに queued のままなら、スーパーバイザーが再起動を繰り返していないかを見る

待ちを長くしながら再起動する#

スーパーバイザー 動き
Kubernetes このページの Deployment のままでよい。kubelet は既定で再起動の前に待ち、回を重ねるごとに上限まで延ばす(しばらく動けば戻る)。通常の終了でも短時間で終わると同じく延びるので、ドレインの多いランナーも CrashLoopBackOff になりうる。判断の前に出力を読む:kubectl logs --previous -n claude-runners deploy/claude-runner(別の Pod は deploy/claude-runner を Pod の名前に替える)
Docker と Compose このページのレシピのままでよい。restart: always は、終了を繰り返すコンテナの待ちを上限まで延ばす。docker inspect --format '{{.RestartCount}}' <container> の回数が増え続けるなら再起動を繰り返している
systemd のユニット 既定では毎回同じ RestartSec を待ち、延ばさない。起動がユニットの上限(既定で10秒に5回)に届くと再起動をやめ、誰かが起動し直すまで止まる(上限の間隔が過ぎるか systemctl reset-failed のあとに再開できる)。RestartSec は通常の終了のあとの再起動も遅らせるので、加減して、再起動の回数で通知する
シェルのループや自前のスーパーバイザー 待ちを5秒から始め、1分未満で終わったら2倍(最大5分)、1分以上動いたら5秒に戻す

終了し続ける理由を確かめる#

起動直後の終了が続くなら、再起動の前に次を見ます。

  • 最後の [runner:fatal] か error: の行:止まった理由が書いてある(よくある原因は上の表)
  • フラグの組み合わせ:Anthropic の git プロキシには --capacity 1 が要る。レシピは容量が大きいので、プロキシを足すなら下げる
  • サービスの環境:手で起動すると動くのにスーパーバイザーの下で失敗するなら、ユーザー・ホームのディレクトリ・PATH・メモリの上限を比べる(--configure-git と git プロキシには PATH 上の git と書き込める ~/.gitconfig が要る)
  • 環境シークレット:失効か打ち間違いなら RegisterRunner auth failed を含む行が出る
  • 環境の「Activity」タブ:新しいランナーが出続けるのにどれも仕事を拾わないなら、再起動を繰り返している

セッションをカスタマイズする#

何も設定しなければ、ランナーはリポジトリを clone し、Claude Code を起動し、後始末をします。既定が合わないときの拡張点(セッションごとの資格情報の用意からチェックアウトの置き換えまで)を挙げます。

  • ラッパーとフックは、ランナーのホスト(Linux か macOS)の実行ファイルとして動く。例は POSIX シェルが前提
  • フックの環境変数の一部は CLAUDE_RUNNER_POOL_ID のように pool と書き、CLI のフラグと環境変数は environment と書く

ラッパースクリプト#

ランナーだけではできない、セッションごとの準備に使います(作成者に絞った短命な資格情報・環境固有の秘密のエクスポート・言語のツールチェーン・子プロセスのリソース制限)。

  • ランナーは Claude Code の代わりに、セッションごとに1回ラッパーを起動する
  • ラッパーは最後に $CLAUDE_RUNNER_CLAUDE_BIN(ランナー自身のバイナリ)へ exec する。シグナルと終了コードが正しく伝わる
  • ランナーの起動時に --exec-path(または SELF_HOSTED_RUNNER_EXEC_PATH)でラッパーを指す
bash
claude self-hosted-runner --environment-secret-file /etc/claude/environment-secret --exec-path /etc/claude/session-wrapper.sh

ラッパーの環境に入る変数です。

変数 内容
CLAUDE_CODE_SESSION_ACCESS_TOKEN sk-ant-cc- で始まるセッションの JWT。act クレームが作成者(記録があればメール)を示す。更新は子の stdin で届くので、ラッパーが見るのは起動時の値だけ
CCR_SESSION_ACCOUNT_EMAIL トークンの act.email を署名の検証なしに取り出した作成者のメール。コミットのトレーラーなどのラベル付け向け。資格情報の発行に使うならトークンを検証してそこから読む。メールが無ければ未設定。個人情報として扱う
CLAUDE_RUNNER_CLIENT_PLATFORM セッションを作った画面(web_claude_ai・desktop_app・ios・claude_code_cli・scheduled_trigger など)。作成時に記録され、ラッパーと全フックが同じ値を見る。分析とラベル付け用で、認可には使わない。不明なら未設定なので set -u では ${CLAUDE_RUNNER_CLIENT_PLATFORM:-} と書く(v2.1.229 以降)
CLAUDE_RUNNER_CLAUDE_BIN ランナー自身の Claude Code の絶対パス。exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@" で、パスを直書きせずに固定の版へ渡せる
CLAUDE_CODE_REMOTE_SESSION_ID cse_... の形のセッション ID。フックの CLAUDE_RUNNER_SESSION_ID(session_...)と同じセッションで、cse_ を session_ に替えると URL の ID になる
CLAUDE_CODE_REMOTE_SESSION_UUID 同じセッション ID の UUID 形式
CLAUDE_SESSION_INGRESS_TOKEN_FILE 最新のセッションの JWT を持つファイルの絶対パス(更新に追従する)。添付のダウンロードで、シェルのサブプロセスが Authorization ヘッダーのために読む。exec なら保たれるが、子の環境を作り直すラッパーが引き継がないと添付のダウンロードが黙って止まる
CLAUDE_CONFIG_DIR セッションごとの Claude の設定ディレクトリ。起動時のホストの設定のスナップショットから書かれ、書き込みはこのセッションに閉じる。--remove-session-state でなければ終了後も <base-dir>/_sessions/ に残る
ANTHROPIC_BASE_URL 子が使う API のベース URL。制御プレーンがセッションごとに渡し、ふつうは https://api.anthropic.com。上書きしない(推論の資格情報は Anthropic の OAuth トークンで、ほかのプロバイダでは通らない)
CLAUDE_CODE_OAUTH_TOKEN 子が推論に使う短命な OAuth トークン。推論とファイルのアップロードに限られ、寿命は約30分。期限前に再発行され子の stdin で届くので、stdin をつながないラッパーは初期値しか見ない。組織の IP 許可リストでは縛れないと考え、漏れれば約30分使えるベアラーとして扱う(ログに出さない・ディスクに書かない・コンテナの外へ送らない)

ラッパーは、サーバーが渡す環境変数を含め、子の環境のほかの値も継承します。exec ならすべて伝わります。別の方法で子を起動するなら、環境を丸ごと渡します。

stdin とファイルディスクリプタ 3 をつないだままにする#

  • 子の stdin はランナーの制御チャネルで、トークンの更新とセッション終了の合図が届く
  • ファイルディスクリプタ 3 にもパイプがあり、ランナーはアイドルと起動のタイムアウトのために子の活動を読む
  • 素の exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@" なら両方保たれる
  • 子を単独の & でバックグラウンドにすると stdin が切れる。最初の OAuth トークンの約30分が過ぎるまでは正常に見え、その後すべての API 呼び出しが 401 authentication_error で失敗する
  • バックグラウンドにする必要があるとき(teardown の trap を残すためなど)は、stdin をファイルディスクリプタ 4 以上に退避してつなぎ直す。ファイルディスクリプタ 3 は閉じない・使い回さない(stdout と stderr のリダイレクトはかまわない)
bash
exec 4<&0
"$CLAUDE_RUNNER_CLAUDE_BIN" "$@" <&4 4<&- &
CHILD=$!
trap 'teardown' EXIT
wait "$CHILD"

システムプロンプトのフラグをそのまま通す#

制御プレーンがセッションに送るシステムプロンプトと追加のシステムプロンプトは、文字列ではなくファイルのパスとしてラッパーに届きます。ランナーが各プロンプトをセッションの設定ディレクトリ(CLAUDE_CONFIG_DIR)のファイルに書き、そのパスを --system-prompt-file <path> か --append-system-prompt-file <path> としてラッパーの引数に入れます。

Claude Code v2.1.281 以降のランナーはファイルで渡します。v2.1.281 より前は、--system-prompt <text> と --append-system-prompt <text> で渡していました。

ラッパーや command フックでは、次のように扱います。

  • そのまま通す:ラッパーの最後を exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@" にする。ほかの引数と同じく、ファイルのフラグも渡る。落とす・書き換えるのはしない。プロンプトのファイルのフラグが失われたセッションは、制御プレーンが送った指示なしで動く
  • v2.1.281 以降のランナーでは、足したファイルのフラグはサーバーのものを置き換え、足し算にはならない:プロンプトのファイルのフラグは値を1つしか取らず、Claude Code は最後の指定を使う。"$@" の後ろに --append-system-prompt-file <path> を足すと、そのファイルの中身がサーバーの追加の指示を置き換える。サーバーの指示に重ねたいなら、ランナーのイメージの CLAUDE.md に書く(ランナーが全セッションのユーザーレベルの設定へ seed する)

セッションの作成者に絞った資格情報を用意する#

decode-token サブコマンドでセッションの JWT のクレームを読めます(トークンは引数・CLAUDE_CODE_SESSION_ACCESS_TOKEN・stdin の順に探す)。次の例は、作成者の ID を短命な AWS の資格情報に替えてから Claude Code へ exec します。

bash
#!/bin/bash
# 安定した Anthropic のユーザー ID で引き、人間の作成者を要求する
CREATOR_SUB=$("$CLAUDE_RUNNER_CLAUDE_BIN" self-hosted-runner decode-token \
  | jq -re '.act.sub // "" | select(startswith("user:"))') \
  || { echo "decode-token: verification failed or no human creator" >&2; exit 1; }

creds=$(your-sts-helper assume-role --subject "$CREATOR_SUB") \
  || { echo "credential exchange failed" >&2; exit 1; }
eval "$creds"

exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@"
  • 認可に使うクレームは jq -r ではなく jq -re で取る。無いときに文字列 null を流さず、非 0 で終わる
  • 組織のサービスの ID(ボットやエージェント)が作ったセッションは user: ではなく agent: の主体を持つので、この例では拒否される。それらも受けるなら、終了するか既定の資格情報へ戻すかを決めておく
  • メールが要るなら .act.email を読み、無い場合も扱う(作成した画面が記録したときだけある。CLI から dispatch したセッションは無いことがある)

ライフサイクルフック#

ランナーのセッションごとの処理の段階を、自分のスクリプトに置き換えます。

  • --hooks-dir <path>(または SELF_HOSTED_RUNNER_HOOKS_DIR)でディレクトリを指す。決まった名前の実行ファイルを探し、無いものは組み込みの動きになるので、要るものだけ置けばよい
  • フックはランナーの権限で動き、セッションの子も同じ UID なので、ディレクトリは読み取り専用でマウントするかイメージに焼く
  • セッションの中で動く Claude Code のフックとは別物で、こちらはランナーの側でセッションの前後に動く

checkout#

リポジトリごとに1回、組み込みの clone と fetch の代わりに動きます。読み取り用の mirror からの clone・アーカイブからの作業ツリー作り・セッション単位の git 認証などに使います。ランナーは次の変数を設定し、表に無い CLAUDE_RUNNER_ の変数を設定することもあります。

変数 内容
CLAUDE_RUNNER_REPO_URL clone するリポジトリの URL(--git-host-rewrite・--git-ssh-rewrite を当てたあと)
CLAUDE_RUNNER_REPO_REF チェックアウトするリビジョン(ブランチ・タグ・コミット SHA)。空ならリポジトリの既定ブランチ
CLAUDE_RUNNER_CHECKOUT_PATH 作業ツリーを置く絶対パス
CLAUDE_RUNNER_SESSION_ID session_... の形のセッション ID(ログの突き合わせ用)
CLAUDE_RUNNER_SESSION_UUID 同じセッション ID の UUID 形式
CLAUDE_RUNNER_API_BASE_URL セッション単位の呼び出しに使う Anthropic API のベース URL
CLAUDE_RUNNER_CLIENT_PLATFORM セッションを作った画面。不明なら未設定
CLAUDE_CODE_SESSION_ACCESS_TOKEN セッション単位の API 呼び出しに使うアクセストークン
GIT_CONFIG_COUNT・GIT_CONFIG_KEY_n・GIT_CONFIG_VALUE_n フックの git のためにランナーが固定する git の設定(Claude Code v2.1.280 以降)。「フックの中の git の設定」を見る

フックが残すもの:

  • 求められたリビジョンの作業ツリーを CLAUDE_RUNNER_CHECKOUT_PATH に置く。HEAD は切り離した状態でよく、作業ブランチはランナーが作る
  • ランナーはそのパスに .git があるかを確かめる。git でないソース(Perforce・展開した tarball)なら、ランナーの環境に CLAUDE_RUNNER_SKIP_GIT_VERIFY=1 を設定する。その場合は作業ブランチや push が使えないので、成果は post-session フックで書き出す

clone の資格情報:

  • ランナーはフックに git の資格情報を渡さない
  • セッションの ID から発行する:CLAUDE_CODE_SESSION_ACCESS_TOKEN を CLAUDE_RUNNER_API_BASE_URL の JWKS で標準の JWT ライブラリで検証し、act クレームの ID 向けの短命な clone の資格情報を自社の資格情報サービスに出させる
  • checkout フックには CLAUDE_RUNNER_CLAUDE_BIN が無いので、decode-token は使えない
  • ホストがすでに持つ git の認証(SSH エージェント・credential helper・.netrc)を使ってもよい

フックが非 0 で終わるか、使えるチェックアウトを残さずに 0 で終わったとき:

  • 結果を push するリポジトリ:セッションを失敗させる。非 0 なら stderr の末尾をユーザーに見せる
  • 読むだけのリポジトリ(実行中に足したものなど):詳細つきの [runner:warn] を出し、Skipped のステップをセッションへ送り、パスに残ったものを消して(すぐ消せなければ終了時に再試行)残りで続ける。0になれば失敗。v2.1.228 より前は、どのリポジトリでも失敗にした

チェックアウトのパスは、セッションの終了後にランナーが消します。

post-session#

子の Claude Code が終わったあと、ランナーが作業領域を片づける前に、セッションごとに1回動きます。

  • コミットしていない作業を残せる唯一の機会。--capacity が1より大きいとフックの直後に worktree が消え、--capacity 1 では正準な clone が次のセッションの開始時にハードリセットされる
  • よくある使い方:未コミットの変更をスナップショットのブランチへ push・ログの保管・自社システムへの終了の通知
  • 子プロセスを起動したセッションなら、終わった理由にかかわらず動く。VM のプリエンプションや停電のような突然の終了では動けないので、それに備えるならセッションの中で Claude Code の PostToolUse フックから定期的にスナップショットを取る
変数 内容
CLAUDE_RUNNER_SESSION_ID session_... の形のセッション ID
CLAUDE_RUNNER_SESSION_UUID 同じセッション ID の UUID 形式
CLAUDE_RUNNER_EXIT_REASON 終わり方(下の4値)
CLAUDE_RUNNER_WORKSPACE_PATHS 作業ツリーの絶対パスのコロン区切り。リポジトリの無いセッションでは空
CLAUDE_RUNNER_DEBUG_LOG_PATH セッションのデバッグログのパス(フックの実行中はディスクにある)
CLAUDE_RUNNER_API_BASE_URL セッション単位の呼び出しに使う Anthropic API のベース URL
CLAUDE_RUNNER_CLIENT_PLATFORM セッションを作った画面。不明なら未設定(v2.1.229 以降)
CLAUDE_CODE_SESSION_ACCESS_TOKEN セッション単位の API 呼び出しに使うアクセストークン
GIT_CONFIG_COUNT・GIT_CONFIG_KEY_n・GIT_CONFIG_VALUE_n フックの git のためにランナーが固定する git の設定(Claude Code v2.1.280 以降)。「フックの中の git の設定」を見る

CLAUDE_RUNNER_EXIT_REASON の値:

  • completed:正常に終わった(プロセスが通常どおり終了したか、動いているあいだにセッションがアーカイブか削除された)
  • failed:プロセスがクラッシュしたか、起動後の準備に失敗した
  • interrupted:ランナーが止めた(枠を空けるための解放・起動のタイムアウト・サーバーによる別のランナーへの移動・ドレイン・--kill-session-after-min の上限)
  • abandoned:別のランナーが取ったセッション用の予約値。いまはこの場合フックは動かない

補足:

  • ライフサイクルのカウンター(下の「セッションのライフサイクルのカウンターの意味」)は、解放・起動のタイムアウト・サーバーによる移動を、枠がきれいに返ったので completed に数える。フックの記録と突き合わせるとこの差が出る
  • フックの終了コードはセッションの結果に影響しない。失敗はログに出して無視する
  • ランナーの停止を含むすべての終了で、最大 --post-session-hook-timeout-sec(既定60秒)待つ

コミットしていない作業を救出用のブランチへ残す例です。

bash
#!/usr/bin/env bash
set -u
IFS=':'
# セッションがチェックアウトの .git/config に仕込みうる設定を固定する
# (-c commit.gpgsign=false は、--configure-git のときでも救出用のコミットに署名を付けないための指定)
g() { git -c core.fsmonitor=false -c core.hooksPath=/dev/null \
        -c commit.gpgsign=false "$@"; }
for ws in $CLAUDE_RUNNER_WORKSPACE_PATHS; do
  cd "$ws" 2>/dev/null || continue
  [ -z "$(g status --porcelain 2>/dev/null)" ] && continue
  g add -A
  g commit -q -m "runner snapshot: $CLAUDE_RUNNER_SESSION_ID ($CLAUDE_RUNNER_EXIT_REASON)" || continue
  g push -q origin "HEAD:refs/heads/rescue/$CLAUDE_RUNNER_SESSION_ID" || true
done

push の資格情報:

  • フックは、ホストの自分の環境にある git の資格情報で push する
  • イメージに資格情報を置かない構成(組み込みの clone が git プロキシを通る場合を含む)では資格情報が無い。push の前に、受け取った CLAUDE_CODE_SESSION_ACCESS_TOKEN を自前のトークンサービスで短命な資格情報に交換する(検証は「セッションの身元を確認する」のとおり)
  • フックがセッションに無い資格情報を持つなら、origin をオペレーターが決めた URL に替え、-c credential.helper= と自前のヘルパーを渡す。セッションが書いた設定がまだ影響しうる範囲は、「フックの中の git の設定」を見る
  • リポジトリ内の credential.helper と pushurl は、そのまま効く。v2.1.280 より前のランナーでは core.sshCommand も効く
ランナーがセッションを解放するときのフックのタイミング

解放したセッションは別のランナーで再開できます。v2.1.236 以降は、解放の時点のセッションの状態で、フックの終了前に再開できるかが決まります。

  • ターンのあとでアイドル、または起動のタイムアウト:子を止め、フックを最後まで走らせてから解放する。フックの最中に送られたメッセージで、別のランナーが再開することはない
  • ユーザーの応答待ち(権限のプロンプトなど):先に解放してからフックを走らせる。フックの最中のメッセージで、別のランナーがフックの終了前に再開しうる

この規則の範囲:

  • アイドルのタイムアウト・--retire-at の時刻・--kill-session-after-min の上限(v2.1.260 以降)など、ランナーが解放するときは常にこの規則
  • ターンが終わって背景タスクだけ残るセッションは、ここではアイドルと数える
  • v2.1.236 より前は、どちらでも先に解放してからフックを走らせた
  • SIGTERM のドレイン中は、フックが終わるまでリースを保つ

フックの中の git の設定#

checkout と post-session のフックは、環境にセッションのアクセストークンを持ったまま動きます。そこで動く git は、セッションが書ける設定ファイル(~/.gitconfig やチェックアウトの .git/config)も読みます。そこでランナーは、どちらのフックの前にも、フックの環境へ git の設定を入れます。GIT_CONFIG_COUNT・GIT_CONFIG_KEY_n・GIT_CONFIG_VALUE_n の組と、git の環境変数です。git はこれらをどの設定ファイルより優先し、効くのはフックが動かす git だけで、セッション自身の git には効きません。ランナーは起動時に [runner:git] lifecycle hooks: の行を出し、効いているフックのパス・許可するプロトコル・gpg のプログラム・署名のモードを示します。Claude Code v2.1.280 以降が要ります。

  • git のフック:値を渡さない限り core.hooksPath は /dev/null。リポジトリの .git/hooks や ~/.gitconfig が指すフックのディレクトリは、git が飛ばす。自分で渡すなら、ランナーの環境に core.hooksPath を GIT_CONFIG_KEY_n・GIT_CONFIG_VALUE_n の組で入れる。ランナーはシステムの git 設定の core.hooksPath も読むが、使うのは、ランナーのユーザーがそのファイル・指すディレクトリ・中のフックのファイルのどれにも書けないときだけ。値を無視したときは、起動時に [runner:warn] の行が値と理由を示す
  • ファイルシステムの監視:core.fsmonitor は空。設定ファイルが指す監視プログラムを、フックの git は動かさない
  • リモートのプロトコル:GIT_ALLOW_PROTOCOL は https:http:ssh。ローカルのパス・file://・git:// の clone・fetch・push は fatal: transport 'file' not allowed か fatal: transport 'git' not allowed で失敗する
  • SSH のコマンドと資格情報の入力:フックの git は設定ファイルの core.sshCommand と core.askPass を無視する。自前の SSH のコマンドを使うなら、ランナーの環境に GIT_SSH_COMMAND を、資格情報の入力プログラムなら GIT_ASKPASS を設定する。セッションはランナーの環境を継承するので、どちらもセッション自身の git にも届く。どちらにも資格情報を入れない
  • gpg のプログラム:gpg.program・gpg.openpgp.program・gpg.x509.program・gpg.ssh.program はランナーが決めるパスで、設定ファイルの値は使わない
  • コミットの署名:--configure-git のときは、フックの中で作るコミットもセッションとして署名される。フラグが無いと commit.gpgsign と tag.gpgsign は false

これらを変えるには、ランナーの環境か、フックの中の git -c を使います。

  • 設定の組:ランナーの環境に入れた GIT_CONFIG_KEY_n・GIT_CONFIG_VALUE_n の組は、同じキーのランナーの値を置き換える。組の番号は 0 から振り、GIT_CONFIG_COUNT に個数を入れる。数が示す最後の組が欠けていると、ランナーは自分の側の組を全部無視し、起動時に [runner:warn] の行を出す
  • git の環境変数:GIT_ALLOW_PROTOCOL・GIT_SSH_COMMAND・GIT_ASKPASS は、ランナーの環境に設定した値のまま残る
  • git -c:フックの中の git -c は、ランナーのものでも自分のものでも GIT_CONFIG_KEY_n の組より優先する。GIT_ALLOW_PROTOCOL・GIT_SSH_COMMAND・GIT_ASKPASS は、git が設定より先に読むので変えられない

ランナーが設定しない項目(credential helper・url.*.insteadOf の書き換え・filter driver など)は、セッションが書ける設定ファイルも含め、すべての設定ファイルから読まれます。その設定ファイルが指す credential helper や filter driver は、フックの権限でプログラムとして動きます。コマンドラインで渡した URL への push を含め、フックの push の行き先も、それらの設定で変わりえます。

v2.1.280 より前のランナーは、これらの設定を何も入れませんでした。--configure-git のとき、フックの中のコミットは、フックが -c commit.gpgsign=false を渡さないと失敗しました。

command#

チェックアウトのあと、セッションごとに1回、組み込みの子の起動の代わりに動きます。

  • ラッパーと同じ環境を受け、同じく "$CLAUDE_RUNNER_CLAUDE_BIN" へ exec する
  • カスタマイズを1つのフックのディレクトリにまとめるなら command フック、ラッパーが別の場所にあるなら --exec-path
  • 両方あると --exec-path が勝ち、command フックは無視される
  • PATH で引いた claude ではなく、必ずランナー自身のバイナリへ exec する(でないと版の固定が効かない)

オンデマンドのランナー#

固定のフリートの代わりに、セッションごとに1つのランナーを起動する方法です。

  • オーケストレーターは状態を持たない別のサブコマンドで、Anthropic から起動の要求(ランナーの無いまま待つセッションごとに1つ)を poll し、要求ごとに spawn-runner フックを実行する
  • フックはプラットフォームへワークロード(Kubernetes の Job・EC2 のインスタンス・Nomad の dispatch)を投入する
  • 資格情報の扱いが良くなる:環境シークレットはユーザーのコードを動かさないオーケストレーターのホストだけに置き、起動した各ランナーは、1つのランナーを登録したら失効する1回限りのワークオーダーを受ける(固定のフリートでは、シークレットが全ランナーのホスト=セッションを動かすホストにある)
bash
claude self-hosted-runner orchestrator \
  --environment-secret-file /etc/claude/environment-secret \
  --hooks-dir /etc/claude/hooks

poll のあいだ状態を持たないので、可用性のために同じ環境へ2つ以上のレプリカを動かせます(各要求はサーバー側でちょうど1つのレプリカが取る)。全レプリカの --expected-spawn-seconds を揃えます。

spawn-runner フック#

起動の要求ごとに ${hooks-dir}/spawn-runner が1回動きます。ランナーの起動を待たずに投入だけ行い、--hook-timeout(既定60秒)以内に戻ります。受け取る変数です。

変数 内容
CLAUDE_RUNNER_WORK_ORDER_FILE 新しいランナーが登録に使う、署名つきのワークオーダーの JWT を持つ一時ファイル。フックの終了後に消える。中身をログに出さない
CLAUDE_RUNNER_ORDER_ID 要求ごとに一意の冪等性のキー(Kubernetes のリソース名にも使える)。重複排除のキーはこれだけにする
CLAUDE_RUNNER_SESSION_ID 対象のセッション。再要求でも同じ値なので、ログと振り分けに使い、重複排除には使わない。--min-idle の事前ウォームの要求では空
CLAUDE_RUNNER_SESSION_UUID 同じセッション ID の UUID 形式。事前ウォームでは空
CLAUDE_RUNNER_ATTEMPT このセッションへの起動の要求の回数。事前ウォームでは 0
CLAUDE_RUNNER_ORDER_SERVER_TIME poll の応答の Date ヘッダーのサーバー時刻。ワークオーダーの exp を確かめるとき、ローカルの時計の代わりに使う。ヘッダーが無ければ空
CLAUDE_RUNNER_POOL_ID 新しいランナーが入る環境の ID(ccpool_...)
CLAUDE_RUNNER_ACCOUNT_ID セッションを積んだアカウントのタグ付き ID(アカウント単位の振り分け・割り当て・課金向け)。無ければ空。Claude Tag のチャンネルのセッションでは常に空
CLAUDE_RUNNER_ACCOUNT_EMAIL セッションを積んだアカウントのメール。無ければ空。個人情報として扱い、ログに出さない
CLAUDE_RUNNER_PRIMARY_REPO_URL 最初の git ソースの URL(そのリポジトリを温めたランナーへ振り分ける用)。無ければ空
CLAUDE_RUNNER_PRIMARY_REPO_REVISION 最初の git ソースのリビジョン(ブランチ・SHA・タグ)。指定が無ければ空
CLAUDE_RUNNER_REPO_SOURCES 全 git ソースの {url, revision} の JSON 配列(2つ目以降で振り分けるとき)。無ければ空
CLAUDE_RUNNER_CORRELATION_ID セッションの作成時に渡された相関 ID をそのまま返したもの(作成のリクエストと結び付けられる)。無ければ空
CLAUDE_RUNNER_CLIENT_PLATFORM セッションを作った画面(web_claude_ai・desktop_app・ios・scheduled_trigger など)。分析用。不明なときと事前ウォームでは未設定なので [ -n "${CLAUDE_RUNNER_CLIENT_PLATFORM:-}" ] で確かめる

起動したランナーは、環境シークレットの代わりにワークオーダーで登録します。

  • 渡し方:--environment-secret-file を JWT のファイルに向けるか、SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET に JWT の値を入れる
  • フックの終了前に JWT を写す:ファイルはフックのあとで消えるので、パスではなく中身を、投入するワークロード(Job 用の Kubernetes の Secret など)へ写す
  • 起動したランナーは --capacity 1:セッションに結びついたワークオーダーはランナーを1つしか登録しないので、容量を上げても使われない枠が増えるだけで、起動時に警告が出る
  • 事前ウォームのワークオーダーはセッションに結びつかない:待機のランナーは、固定のフリートのようにキューから仕事を取る

プロビジョナーを問わず守る4つの約束です。

  1. CLAUDE_RUNNER_ORDER_ID で冪等にする:同じ要求が再配信されても起動するのは多くても1つ。注文 ID から決まったリソース名を作り、重複はプラットフォームに拒ませる。CLAUDE_RUNNER_SESSION_ID で引かない(再要求は同じセッション ID と新しい注文 ID で来るので、セッション ID の名前では2回目が作られない)
  2. ワークロードを再試行しない:1つの注文 ID で作るのは多くても1つ。ランナーが登録しなければ、Anthropic が --expected-spawn-seconds のあとに新しい注文 ID で要求し直す
  3. 終了コードを守る:0 は投入済み、1 は再試行できる失敗(間を置いて要求し直される)、2 以上は再試行できない失敗(環境の「Activity」タブで Owner が「Retry」を押すまで起動されない)。非 0 では stderr の末尾が理由としてそこに出るので、対処の分かるエラーを書き、秘密は書かない。事前ウォームの要求には失敗させるセッションが無く、非 0 はローカルに記録されるだけで、リースのあとサーバーが要求し直す
  4. --expected-spawn-seconds を起動時間の p99 以上にする:サーバー側のリース。全レプリカで同じ値にする

調べ方:

  • フックの stdout と stderr は、資格情報を伏せてオーケストレーターのログに出る
  • queued のままなら、オーケストレーターの /healthz のキューの数を見て、環境の「Activity」タブで失敗したセッションを開いて起動のエラーを読み、「Retry」で要求し直す
  • エラーが無いのに queued のままなら、フックがセッション ID で引いている疑いがある。そのセッションの最初のワークロードだけがあって再要求のものが無ければ、CLAUDE_RUNNER_ORDER_ID で引くように直す

Bedrock や Agent Platform へモデルのリクエストを送る#

モデルのリクエストを自社の AWS か Google Cloud のアカウントを通したいときは、ランナーを Amazon Bedrock か Google Cloud の Agent Platform(旧 Vertex AI)向けに設定します。そのランナーが始めるすべてのセッションが、自社のクラウドの資格情報で、自社のクラウドアカウントのモデルを呼びます。この設定が無ければ、セッションは Anthropic の API へリクエストを送ります。

  • ランナーは引き続き Anthropic にセッションを poll し、各セッションはイベントストリームを api.anthropic.com へ送る。ストリームにはプロンプト・応答・ツールの結果が載る。プランの条件と Zero Data Retention を除外する点は、そのまま当てはまる
  • セッションはランナーではなく環境へ振り分けられ、キューへ戻したセッションや再開したセッションは別のランナーで動きうる。環境のすべてのランナーを同じ設定にする
  • 始める前に、下の「Anthropic の API のセッションと違うこと」を読む

手順です。

  1. クラウドアカウントと外向きのルールを用意する:モデルへのアクセス・絞ったポリシーかロール・ネットワークの許可を整える
    • Amazon Bedrock:ユースケースを申請し、IAM のポリシーを作る。bedrock:InvokeModel と bedrock:InvokeModelWithResponseStream は、セッションが使う推論プロファイルと、その裏の基盤モデルだけに絞る
    • Agent Platform:API を有効にしてモデルのアクセスを申請し、aiplatform.endpoints.predict だけを持つカスタムロールを作る
    • 外向き:使うプロバイダのエンドポイントを外向きのルールで許可する(「通信先の許可」)。届かないと、Claude Code が数時間リトライしてからセッションにエラーが出ることがある
  2. セッションに絞った資格情報を渡す:手順1のポリシーかロールを、ほかに何もできない ID に付ける。

注意

セッションでコードを動かせる人(プロンプトインジェクションを含む)は、資格情報が有効なあいだ、あなたの費用でこの資格情報を使えます。Claude Code はセッションの中で動くので、モデルを呼ぶ資格情報はそこから読めなければなりません。Claude が実行するシェルのコマンド・フック・stdio の MCP サーバーは、セッションの環境を継承し、Claude Code と同じユーザーで動くので、資格情報の変数やファイルを読めます。強化の項でホストの資格情報をセッションから遠ざけても、この資格情報だけは遠ざけられません。裏の ID には、手順1のポリシーかロール以外を持たせないでください。

選ぶ方法を、ランナーの次の挙動と照らします。

  • メタデータのエンドポイント:セッションからクラウドのメタデータのエンドポイントを完全に拒むと、そこから配られる資格情報(インスタンスプロファイルなど)も Claude Code に届かない。ファイルで渡す web identity(Amazon EKS の IRSA や Workload Identity Federation の資格情報ファイル)は、それに頼らない
  • 更新:セッションは資格情報より長く続きうるので、ファイルの web identity のように自分で更新する方法を使う
  • ラッパースクリプト:ランナーはラッパーをセッションごとに1回起動するので、ラッパーが export した資格情報は更新されない。Claude Code は AWS の資格情報を環境から読むので、別の用でラッパーが AWS の資格情報を export していると、Claude Code はそれでモデルのリクエストに署名しうる
  1. ランナーの環境に、片方のプロバイダの変数を1組だけ設定する:コンテナの仕様やサービスユニットなど、ランナーのほかの環境変数を置く場所に入れ、ランナーを再起動する(オンデマンドのランナーでは、spawn-runner フックが起動するワークロードに入れる)。これらのランナーは --confine-repo-settings enforce で起動する。コミットされた設定に印が付くリポジトリのセッションを拒むので、先に既定の warn で動かして、出たログを片づける。

    Amazon Bedrock(リージョンは自分のものに替える):

    bash
    export CLAUDE_CODE_USE_BEDROCK=1
    export AWS_REGION=us-east-1
    

    Google Cloud の Agent Platform(リージョンとプロジェクト ID は自分のものに替える):

    bash
    export CLAUDE_CODE_USE_VERTEX=1
    export CLOUD_ML_REGION=global
    export ANTHROPIC_VERTEX_PROJECT_ID=YOUR-PROJECT-ID
    
  2. 変数がセッションに届いたかを確かめる:ホストの自分のシェルは別のプロセスなので、セッションの中で確かめる。環境でセッションを始め、Claude に次のコマンドを実行させる。

    bash
    env | grep -E 'CLAUDE_CODE_USE_(BEDROCK|VERTEX)'
    

    CLAUDE_CODE_USE_BEDROCK か CLAUDE_CODE_USE_VERTEX が 1 の行が出れば、届いている。両方出たときは Amazon Bedrock が使われる。何も出なければ、どちらも届いていない。このコマンドが示すのは設定で、通信ではない。リクエストそのものは、クラウドアカウントのメトリクスやリクエストログで確かめる。

Anthropic の API のセッションと違うこと#

モデルのリクエストを Amazon Bedrock か Agent Platform へ送るセッションは、次の点で Anthropic の API のセッションと違います。

  • claude.ai のポリシー:サーバー管理設定は届かない。Owner が Claude Code の管理設定で決める組織のポリシーも届かないので、セッションの中で Claude Code は強制しない。頼りにするルールは、ランナーのイメージの管理設定ファイルに書く
  • ファイル:claude.ai やモバイル・デスクトップのアプリでセッションに添付したファイルは届かず、Claude は SendUserFile ツールでファイルを返せない。入力ファイルはリポジトリかランナーに置く
  • モデルの選択:セッションのモデルは Anthropic の制御プレーンが送り、無いときは Claude Code がプロバイダの既定を使う。ランナーはセッションへ渡す環境から ANTHROPIC_MODEL と ANTHROPIC_DEFAULT_MODEL を取り除くので、ランナーの環境ではどちらも効かない。ファミリーごとの変数(Bedrock と Agent Platform の「モデルの版を固定する」の項)はセッションに届き、opus のようなエイリアスの解決先を決める(完全なモデル ID の解決先は決めない)
  • アカウントが提供しないモデル:メッセージの途中で、モデル名を挙げたエラーで失敗することがある。開発者が選べるモデル・背景のモデル・auto モードが使う分類器のモデルを有効にする。Amazon Bedrock ではそれぞれをポリシーで許可する
  • Web 検索と fast mode:Web 検索は Amazon Bedrock では使えず、fast mode はどちらのプロバイダでも使えない。ほかのプロバイダ別の違いは /cloud-providers を見る

MCP サーバー#

全セッションで MCP サーバーを使うには、イメージのビルド時に claude mcp add を実行します(手元のインストールと同じコマンド)。

  • --scope user が必須(既定のローカルのスコープは、ランナーが seed しないキーに書かれる)
  • ランナーが素のプロセスなら、ホストでランナーのユーザーとして実行し、ランナーを再起動する(ホストの設定は起動時に1回読まれる)
dockerfile
RUN claude mcp add --scope user sidecar -- /usr/local/bin/mcp-sidecar
RUN claude mcp add --scope user --transport http internal http://mcp-gateway.svc.cluster.local:8080

ホストの設定の取り込み:

  • 起動時にホストの .claude.json(~/.claude/ の中ではなく隣)の mcpServers キーだけを取り込み、各セッションの設定へ seed する(アカウントの状態とプロジェクトの履歴は捨てる)
  • type を認識できない項目は、起動時の警告に出して捨てる(サーバーが無い理由が分かる)
  • SELF_HOSTED_RUNNER_HOST_CONFIG_DIR を設定すると、そのディレクトリの .claude.json を読む。空のディレクトリを指せば MCP の seed も止まる
  • 届いたかは、環境でセッションを始めて Claude に MCP のツールを一覧させて確かめる

ほかの読み込み元:

  • エンタープライズスコープの管理された MCP ファイル(Linux は /etc/claude-code/managed-mcp.json、macOS は /Library/Application Support/ClaudeCode/managed-mcp.json):管理者が挙げたサーバーだけに限る強い管理向け。ホストにあると、制御プレーンが配る MCP サーバー(claude.ai のコネクタを含む)を読まず、子の stderr の警告に名前を出す(ランナーは debug のログに記録)。v2.1.229 より前は、こうしたセッションが You cannot dynamically configure MCP servers when an enterprise MCP config is present で起動時に終了した
  • ランナーのホストの managed settings の managedMcpServers:ほかを締め出さずに HTTP と SSE のサーバーを足す。ほかの読み込み元のサーバーも読まれる(v2.1.259 以降)
  • <repo>/.mcp.json:プロジェクトのスコープ。リポジトリにコミットすれば、クラウドセッションでは自動で承認される

あわせて:

  • 組織でコネクタの配信が有効なら、claude.ai で設定したコネクタが、対話で作ったセッションへ api.anthropic.com 経由で配られる。CLI の dispatch のようにプログラムで作ったセッションには配られないので、上のほかの方法で渡す
  • settings.json には MCP サーバーを書けない(トップレベルの mcpServers は無い。managed settings では managedMcpServers)
  • MCP のツール検索は、セッションが継承するランナーの環境の ENABLE_TOOL_SEARCH で制御する

組み込みのセッションのツールを止める#

制御プレーンは、Claude Code Remote という自前の MCP サーバーをクラウドセッションに付けます。Claude はこれでルーティンの予約・ほかのクラウドセッションの開始と操作・リポジトリの追加・プルリクエストの追従をします。

  • サーバーごと止めるには、設定にサーバー単位の deny ルールを足す
  • サーバー名はセッションの作られ方で3通りあり、照合は大文字小文字まで厳密なので、3つとも書く
json
{
  "permissions": {
    "deny": [
      "mcp__Claude_Code_Remote",
      "mcp__claude-code-remote",
      "mcp__bf7c680d-5fdc-5ef4-b4a0-abadb619bf0a"
    ]
  }
}
  • サーバー単位のルールは、あとから増えるツールにも効く
  • 1つのツールだけ止めるなら、末尾にアンダースコア2つとツール名を足す(mcp__Claude_Code_Remote__add_repo)
  • 接続そのものを止めるなら、3つの名前を mcp__ を外して deniedMcpServers の serverName に足す
  • 置き場所は、ランナーを変えずにセッションへ届くサーバー管理設定か、ランナーの ~/.claude/settings.json。モデルのリクエストを Bedrock か Agent Platform へ送るランナーでは、サーバー管理設定がそのセッションに届かないので、後者を使う
  • 効いたかは、セッションで MCP のツールを一覧させて確かめる(拒否したツールは Claude のコンテキストから消える)

セッションに作業を push させる#

Anthropic ホストのセッションでは、Claude にコミットと push を促す Stop フックが動いています。ランナーはこれを入れません。無いと、未コミットのまま終わったセッションの作業はランナーのディスクにしか残らず、claude.ai/code の「Create PR」もブランチがリモートに出るまで押せません。

参照実装は2つの部品です。設定のブロックをランナーのホストの ~/.claude/settings.json(全セッションへ seed される)に足し、スクリプトを ~/.claude/hooks/stop-hook-nudge.sh に置いて実行可能にします。

json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "timeout": 10,
            "command": "\"$CLAUDE_CONFIG_DIR/hooks/stop-hook-nudge.sh\""
          }
        ]
      }
    ]
  }
}

スクリプトの動き:

  • プロジェクトのディレクトリに未コミットの変更(.claude/ の下を除く)か未 push のコミットがあれば、{"decision":"block","reason":"..."} を stdout に出して Claude に促す
  • 促すのはターンごとに1回だけ(stop_hook_active で再入を防ぐ)
  • git のリポジトリでない・リモートが無いときは何もしない

権限とツールの承認#

セルフホストのセッションには端末が無いので、答えのない権限のプロンプトは、ユーザーが UI で答えるまでターンを止めます。

  • 制御プレーンが、各セッションのツール一覧と権限ルールを仕事と一緒に送る
  • 既定の設定は Bash を含む定型のツール呼び出しを事前承認する。クラウドセッションはモードにかかわらずファイルの編集を事前承認する(権限モード)
  • どれにも当たらない呼び出しは、セッションの UI でプロンプトを出す

注意

auto モードを固定するのは、セッションのコンテナの外向きが既定で拒否され、強化の項のほかの対策も済んだ環境だけにしてください。Bash のネットワークリクエストを含む定型の呼び出しは、既定の事前承認でも auto モードでも人を通さずに動くので、届く範囲を絞れるのはネットワークの境界だけです。

プロンプトを最小にするには、ラッパーか command フックで auto モードを固定します。

  • auto モードでは、別の分類器モデルが実行前にアクションを見て、却下したものをブロックする。明示の ask ルールは引き続きプロンプトを出す
  • ランナーはサーバーが決めたフラグをラッパーの前に足す。--permission-mode のような単一の値のフラグは最後の指定が勝つので、"$@" の後ろに足せばサーバーの値を上書きできる
bash
#!/bin/bash
exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@" --permission-mode auto
  • 特定のツールを事前承認するなら --allowed-tools を足す(例 --allowed-tools "Bash(bazel *) Bash(yarn *) mcp__internal__*")
  • --allowed-tools と --disallowed-tools のような一覧のフラグは累積するので、制御プレーンのルールに重なる
  • 絞るには --disallowed-tools を足す(ほかのルールが許していても拒否する)

各セッションの設定がどう組み立てられるか#

ランナーはセッションごとに設定ディレクトリを与え、起動時に1回取るホストの ~/.claude/ のスナップショットから seed します。

  • イメージの settings.json・CLAUDE.md・フック・エージェント・コマンド・スキルが、全セッションのユーザーレベルの基準になる
  • 動いているホストの設定を変えても、ランナーを再起動するまで効かない
  • 別のパスから seed するなら SELF_HOSTED_RUNNER_HOST_CONFIG_DIR。空のディレクトリを指すと seed しない
  • リポジトリにコミットされた .claude/settings.json は、プロジェクトの設定として上に重なる
  • Claude Tag 以外のセッションでは、セルフホスト環境のセッションの auto memory は既定でオフ。セッションをまたいで残したい指示は、ランナーのイメージかリポジトリの CLAUDE.md に書く
  • ランナーが取るホストの ~/.claude/ のスナップショットは projects/ ディレクトリを含まない。auto memory の既定の保存先はその下なので、そこへメモリのファイルを置いてもセッションへ seed されず、auto memory もオンにならない
  • イメージの標準のシステムパスの managed-settings.json も読む。ただし既定では、組織がサーバー管理設定のキーを1つでも配ると、イメージのファイルは無視される。例外は、すべての管理の源から読むキー(env ブロック・サンドボックスのロックとバイナリのパス・forceRemoteSettingsRefresh)(設定ファイルの仕組み)

制御プレーンがセッションに Claude Code のフックを渡すと、ランナーは自分の設定と混ぜずに並べて入れます(v2.1.229 以降)。

  • 置き場所:渡されたスクリプトはセッションの設定ディレクトリの予約の hooks/.ccr-launcher/ に書き、--settings で渡す別の設定ファイルに登録する。seed された settings.json と自分の hooks/<name> は変えない。予約のディレクトリは毎回作り直し、ホストの ~/.claude/hooks/.ccr-launcher/ は seed しない
  • 中身:制御プレーンが自分の固定の定数から作る(セッションごとの入力や第三者の入力からは作らない)
  • 効き方:--settings のフックは managed の階層ではなく通常のフック設定に入るので、managed settings は引き続き効く。disableAllHooks で無効になり、allowManagedHooksOnly で残るものには入らない

リポジトリにコミットした権限ルール#

  • リポジトリの permissions.allow に、素の "Edit"・"Write"・"NotebookEdit" を入れない。パスを問わず一致し、ホストのどこへでも書けてしまうので、confine ガードがセッションに印を付ける(--confine-repo-settings enforce なら起動を拒否する)
  • リポジトリにファイルツールのルールは要らない(クラウドセッションはモードにかかわらず編集を事前承認する)。入れるなら "Edit(/**)" のようにワークスペースに絞る(先頭のスラッシュ1つはプロジェクトのルートから)
  • 素のファイルツールのルールは、リポジトリに入らないオペレーターのホストの settings.json ならかまわない
  • defaultMode の auto は、イメージ全体かユーザーレベルの設定からしか効かない。チェックアウトしたリポジトリが自分で auto モードにすることはできない

全フラグと設定項目#

インストールした版の正式な一覧は claude self-hosted-runner --help です。ランナーとオーケストレーターは Linux か macOS のホストで動き、/workspace や ~/.claude のような既定もそれが前提です。

ランナーの CLI フラグ#

  • 多くのフラグに対応する環境変数があり、両方あればフラグが勝つ
  • 期間はフラグでは分か秒、環境変数は名前の _MS のとおり常にミリ秒(表の既定はフラグの単位)
  • 例:--exit-if-unused-min 10 は SELF_HOSTED_RUNNER_IDLE_SHUTDOWN_MS=600000 と同じ。Helm の値の SELF_HOSTED_RUNNER_STARTUP_TIMEOUT_MS: "15" は15分ではなく15ミリ秒
フラグ 環境変数 既定 内容
--api-url <url> なし https://api.anthropic.com API のベース URL。上書きはテストのときだけ
--base-dir <path> SELF_HOSTED_RUNNER_BASE_DIR /workspace(Windows は無し) チェックアウトとセッションごとの作業ディレクトリの置き場。このパスかその親への書き込み権限が要る。起動時に作り、だめなら cannot create or write to base directory で終了(v2.1.225 より前は最初のセッションで作った)。Windows は非対応で既定が無く、指定しないと起動時に終了。環境の全ランナーで揃える
--capacity <n> なし 1 同時に扱うセッションの最大数(すべてロックした所有者のもの)。環境の全ランナーで揃える
--client-label <label> SELF_HOSTED_RUNNER_CLIENT_LABEL ホスト名 登録時に送るラベル。claude_code_self_hosted_runner_info の client_label にも出る(v2.1.248 以降)
--configure-git SELF_HOSTED_RUNNER_CONFIGURE_GIT=1 オフ 起動時に git の ID・Anthropic のコミット署名・push の交渉(v2.1.257 以降)・Co-authored-by: を足すフックを設定する
--confine-repo-settings <mode> SELF_HOSTED_RUNNER_CONFINE_REPO_SETTINGS warn リポジトリの設定が、ワークスペースの外の読み書きを許す・環境変数を設定する・sandbox.enabled: false や disableAllHooks などオペレーターの設定を上書きするときの扱い。warn(記録して起動)・enforce(拒否)・off(調べない)
--debug-token-dir <path> SELF_HOSTED_RUNNER_DEBUG_TOKEN_DIR 未設定 調べるために有効なトークンをディスクへ書く。デバッグ専用で、本番では使わない
--defer-shutdown-max-min <n> SELF_HOSTED_RUNNER_DEFER_SHUTDOWN_MAX_MS 0 最初の SIGTERM・SIGINT のあとも持っているセッションを提供し続け、N分後に残りを解放して終了する。先にホストの停止のタイムアウトを延ばす。0 で無効(v2.1.238 以降)
--drain-grace-sec <n> SELF_HOSTED_RUNNER_DRAIN_GRACE_MS 0 停止の合図も退役時刻も無いとき、セッションが終わったあとの扱い。0 はすぐ終了、正の値はその秒数だけロックした所有者のキューを poll する(コンテナの分離は弱まる)。--defer-shutdown-max-min で遅らせた最初の合図のあとは、これにかかわらずセッションが0になりしだい終了
--drain-marker-file <path> SELF_HOSTED_RUNNER_DRAIN_MARKER_FILE 未設定 ホストが SIGTERM の前に書く、穏やかなドレインの目印のファイル。ドレインの開始時にあれば、終了を単なる停止ではなくホストのドレインとして Anthropic へ報告する(ドレインの動きは同じ)。セッションが書けないローカルのパスにする(v2.1.271 以降)
--drain-wait-sec <n> SELF_HOSTED_RUNNER_DRAIN_WAIT_MS 0 ドレインの開始後(--defer-shutdown-max-min が無ければ SIGTERM で)、子を止める前に、進行中のターンと背景タスクを最大N秒待つ。終わったばかりの背景タスクは、結果を読むターンが始まるまで(最大 SELF_HOSTED_RUNNER_BG_RESULT_GRACE_MS)実行中と数える
--environment-secret-file <path> SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET 必須 環境シークレットのファイル(オーケストレーターが起動したランナーではワークオーダーの JWT)。環境変数はパスではなく値そのもの。古い --pool-secret-file・SELF_HOSTED_RUNNER_POOL_SECRET も動くが stderr に非推奨の通知が出る(2.1.216 より前のプレビューのビルドは古い名前だけ)
--exec-path <path> SELF_HOSTED_RUNNER_EXEC_PATH 自分のバイナリ セッションごとに起動するバイナリかラッパー
--exit-if-unused-min <n> SELF_HOSTED_RUNNER_IDLE_SHUTDOWN_MS 0 一度も仕事が来ないまま N 分 poll したら終了する(オートスケーラーの縮小向け)。0 で無効
--git-host-rewrite <from>=<to> なし 未設定 clone の前に https://<from>/... を https://<to>/... に書き換える(split-horizon の DNS 向け)。繰り返し可。フラグのみ
--git-ssh-rewrite <host> なし 未設定 clone の前に https://<host>/... を git@<host>:... に書き換える(SSH だけのホスト向け)。繰り返し可。フラグのみ
--health-port <port> SELF_HOSTED_RUNNER_HEALTH_PORT 8080 /healthz と /metrics のポート。0 で無効
--hooks-dir <path> SELF_HOSTED_RUNNER_HOOKS_DIR 未設定 ライフサイクルフックのディレクトリ
--host-config-snapshot <mode> SELF_HOSTED_RUNNER_HOST_CONFIG_SNAPSHOT disk 起動時に取るホストの設定のスナップショットの置き場。disk は --base-dir の下へ写し、セッションの開始ごとにメモリ上のダイジェストと照合する(改変があればそのセッションは失敗し、再起動まで受け付けない)。memory はヒープに持ち、上限 64 MiB(超えるとホストの設定なしで始まり、通知が出る)。ディスクに書けなければログに出してその回は memory(v2.1.271 以降)
--kill-session-after-min <n> SELF_HOSTED_RUNNER_MAX_LIFETIME_MS 0 止まったセッションへの備えとして、セッションを壁時計で N 分に制限する。v2.1.260 以降は上限で解放し(次のメッセージで再開できる)、SELF_HOSTED_RUNNER_MAX_LIFETIME_GRACE_MS の猶予後も残るときだけ終了(それより前は上限で終了)。0 で無効
--lock-to-account <id> SELF_HOSTED_RUNNER_LOCK_TO_ACCOUNT 未設定 最初のセッションを待たず、起動時に特定のアカウントへロックする。組織のメールアドレスか user_... の ID。アカウントの無い Claude Tag のチャンネルセッションは拾わない
--log-file <path> SELF_HOSTED_RUNNER_LOG_FILE 未設定 stdout と stderr に加え、0600 で作るファイルにもログを書く。self-hosted-runner doctor がローカルで tail するのに要る
--log-level <level> なし info info か debug
--post-session-hook-timeout-sec <n> SELF_HOSTED_RUNNER_POST_SESSION_HOOK_TIMEOUT_MS 60 post-session フックの持ち時間(ランナーの停止を含むすべての終了で)
--proxy-authorization-command <command> SELF_HOSTED_RUNNER_PROXY_AUTHORIZATION_COMMAND 未設定 外向きプロキシへの接続ごとに実行し、stdout(前後の空白を除く)を Proxy-Authorization にする。HTTPS_PROXY か HTTP_PROXY が要り、--proxy-authorization-file と併用不可(v2.1.238 以降)
--proxy-authorization-file <path> SELF_HOSTED_RUNNER_PROXY_AUTHORIZATION_FILE 未設定 接続ごとにファイルを読み、中身(前後の空白を除く)を Proxy-Authorization にする。別のプロセスが回すトークン向け。条件はコマンド版と同じ(v2.1.238 以降)
--push-outcome-on-release SELF_HOSTED_RUNNER_PUSH_OUTCOME_ON_RELEASE オフ ランナーが始めた終了(ドレインやアイドルの解放)で、作業領域を消す前に結果のブランチを origin へ push し、コミットを再起動の先へ残す。ベストエフォート。停止の予算に30秒足され、push したブランチからの再開に git 2.29 以降が要る。先に claude/* への push を制限する。checkout フックのリポジトリは push しないので post-session で残す
--release-idle-session-min <n> SELF_HOSTED_RUNNER_SESSION_IDLE_MS 0 ターンの終了かユーザー待ちのあと、N 分動きが無ければ枠を解放する。ターンの途中(終わらない背景タスクや承認待ちを含む)はアイドルと数えないので、--kill-session-after-min と組む。背景タスクの終了後は、結果を読むターンが始まるまで(最大 SELF_HOSTED_RUNNER_BG_RESULT_GRACE_MS)ビジー扱い。合図も退役時刻も無いときに解放でセッションが0になると、--drain-grace-sec の通常の終了に進む。遅らせた最初の合図のあとは、セッションが0になりしだい終了。0 で無効
--remove-session-state [bool] SELF_HOSTED_RUNNER_REMOVE_SESSION_STATE オフ 結果にかかわらず、終了したセッションの <base-dir>/_sessions/ のディレクトリを消す。ベストエフォート(消す前の kill やドレインの期限切れでは残る)。オンだと失敗・中断したセッションのデバッグログも残らない(v2.1.268 以降)
--retire-at <epoch-seconds> SELF_HOSTED_RUNNER_RETIRE_AT 未設定 Unix 時刻(秒)でランナーを退役させる(決まった時刻に kill するインフラ向け)。2001年より前か5138年より後は、フラグでは拒否、環境変数では無視
--session-stop-grace-sec <n> SELF_HOSTED_RUNNER_SESSION_STOP_GRACE_MS 5 セッションの終了後、強制終了の前に Claude のプロセスが自分で終わるのを待つ時間。子の SessionEnd フックに時間が要るなら上げる
--startup-timeout-min <n> SELF_HOSTED_RUNNER_STARTUP_TIMEOUT_MS 15 子が起動から N 分以内に初期化の合図を出さなければ枠を解放する。通常の出力ではなく初期化の合図で解け、以降は --release-idle-session-min が引き継ぐ。0 で無効
--trust-workspace [bool] SELF_HOSTED_RUNNER_TRUST_WORKSPACE オン 各リポジトリのパスに信頼を与え、リポジトリの permissions.allow と additionalDirectories を効かせる。false ならそれらを捨て、許可はホストの settings.json で設定する。リポジトリの sandbox.* はどちらでも効く
--use-anthropic-git-proxy CLAUDE_RUNNER_USE_GIT_PROXY=1 オフ 自社の git 認証の代わりに Anthropic の git プロキシで clone する。--capacity 1 と git 2.32 以降が要り、満たさないと起動を拒否。書き換えのフラグより優先

期間のフラグの上限:

  • ランタイムの32ビットのタイマー(約24.85日)に収まるよう、多くに上限がある
  • --*-min は10080分(7日)、--drain-grace-sec は604800秒(7日)、--drain-wait-sec は86400秒(24時間)。--session-stop-grace-sec と --post-session-hook-timeout-sec は上限なし
  • 超えると、フラグなら起動がエラーで失敗し、環境変数なら上限に丸められる

オーケストレーターの CLI フラグ#

self-hosted-runner orchestrator は、--api-url・--environment-secret-file・--hooks-dir・--health-port・--log-level をランナーと同じ既定(同じ環境変数)で受けます。ただし --hooks-dir は必須で、spawn-runner フックが要ります。独自のフラグです。

フラグ 既定 内容
--hook-concurrency <n> 4 同時に動かす spawn-runner フックの最大数。1回の poll で取る要求の数の上限も兼ねる
--hook-timeout <sec> 60 この秒数でフックのプロセスツリーを止める。これに5秒の kill の猶予を足しても --expected-spawn-seconds 未満であること(起動時に検査)
--expected-spawn-seconds <sec> 120 起動したランナーの起動時間の p99(サーバーが受ける範囲は10〜3600)。poll ごとにサーバー側のリースとして送られ、それまでに登録が無ければ新しい注文 ID で要求し直される。全レプリカで揃える
--min-idle <n> 0 待機のランナーを先に起動し、空きの枠を少なくとも N 個保つ。0 で事前ウォームなし。余りが自分で消えるよう、ランナーの --exit-if-unused-min と組む
--debug-dir <path> 未設定 要求ごとのワークオーダーとフックの stderr をディスクへ書く。デバッグ専用で、本番では使わない

SCM コネクタのフラグ#

SCM コネクタはいまは使えないので、これらは設定しません。--scm-connector-host を設定しても接続は開かず、オーケストレーターは再試行し続けます(ランナーはセッションが来るたびに起動する)。コネクタは、オーケストレーターから制御プレーンへの常時の WebSocket で、リポジトリの選択やブランチ・ref の解決といったセッション前の処理を、社内からしか届かない GitHub Enterprise Server へ届ける設計です。

フラグ 既定 内容
--scm-connector-host <host[:port]> 未設定 転送先の GitHub Enterprise Server のホスト名。ポートの既定は 443
--scm-connector-id <n> --scm-connector-host と併用で必須 組織の GitHub Enterprise Server の接続の数値 ID
--scm-connector-provider <slug> ghe プロバイダを表すパスの区間。^[a-z0-9-]{1,32}$ に一致すること
--scm-connector-ca-file <path> 未設定 GitHub Enterprise Server への TLS で使う追加の CA バンドル(PEM)
--scm-connector-host-rewrite <from>=<to_host:to_port> 未設定 エンドツーエンドのテスト専用。Host ヘッダーと TLS の SNI は --scm-connector-host のまま、TCP の接続先だけ替える

接続のたびに既存の環境シークレットを送り、失敗すると指数バックオフ(上限30秒とジッター)で自動で再試行します。

環境変数だけの設定#

次は環境変数からだけ読まれます。多くの配備では既定のままです。

環境変数 既定 内容
SELF_HOSTED_RUNNER_BG_RESULT_GRACE_MS 30000 背景タスクが終わってから、結果を読むターンが始まるまでセッションをビジーと見なす時間。0 や不正な値は既定に戻る(無効にはできない。v2.1.228 以降)
SELF_HOSTED_RUNNER_HOST_CONFIG_DIR ~/.claude 起動時のスナップショットに取り込み、各セッションの CLAUDE_CONFIG_DIR へ seed するディレクトリ。変更は再起動後に効く。設定すると MCP の seed で読む .claude.json の場所も移る(既定と同じ値でも)。空のディレクトリで seed を完全に止められる
SELF_HOSTED_RUNNER_MAX_LIFETIME_GRACE_MS 900000 --kill-session-after-min の上限のあと、ターンの終了か解放を待ってから終了するまでの時間
SELF_HOSTED_RUNNER_POST_TURN_SETTLE_MS 7000 ターンの終了を Anthropic へ報告するあいだ、--drain-wait-sec のドレインでビジーと数える時間の上限。0 や不正な値は既定に戻る(v2.1.275 以降)
SELF_HOSTED_RUNNER_SIGKILL_GRACE_MS 30000 割り込めない I/O で止まった子へ SIGKILL が届くのを、自分が終了する前に待つ時間。下限は --post-session-hook-timeout-sec+15秒(--push-outcome-on-release でさらに30秒)なので、既定での実効の最小は75秒
CLAUDE_RUNNER_FETCH_DEPTH 50 新しい clone の fetch の深さ。正の整数、または完全な fetch なら full か 0。既存のリポジトリは今の深さのまま
CLAUDE_RUNNER_SKIP_GIT_VERIFY 未設定 1 で、checkout フックのあとの .git の確認を飛ばす(git でないソース向け)
FORCE_AUTOUPDATE_PLUGINS 未設定 1 で、バイナリを固定していてもプラグインのマーケットプレイスの自動更新を許す
CLAUDE_CODE_DISABLE_ARTIFACT 未設定 1 で、組織の設定にかかわらずアーティファクトのツールを無効にし、*.frame.claudeusercontent.com への外向きを不要にする
CLAUDE_CODE_BYOC_ENABLE_DATADOG 未設定 1 で Datadog の運用メトリクスにオプトインする(セルフホストでは既定オフ)

ヘルスのエンドポイント#

ランナーはヘルスポートで GET /healthz を出します。poll の状態にかかわらず、プロセスが生きていれば 200 OK なので、HTTP プローブで分かるのは死んだプロセスだけです。本文の JSON が今の状態です。

json
{
  "status": "ok",
  "runner_id": "ccrunner_...",
  "active_sessions": 2,
  "last_poll_at": "2026-03-31T18:04:11.220Z",
  "last_poll_age_ms": 842
}
  • last_poll_age_ms を自前のプローブの liveness に使う(増え続けるなら poll が止まっている)
  • last_poll_at と last_poll_age_ms は、最初の poll が終わるまで null
  • オーケストレーターの /healthz は常に 200。本文の connected(直近の poll が成功したか)と queue_counts(状態ごとの起動キューの数)を見て、準備の判定と通知は connected で行う
  • SCM コネクタを設定すると、scm_connector_connected と scm_connector(connected・last_connected_at・last_error・reconnects・requests_forwarded)も載る(--scm-connector-host が未設定ならどちらも null)

Prometheus のメトリクス#

各ランナーは /healthz と同じポートの GET /metrics で出します。主なシリーズです。

シリーズ 備考
claude_code_self_hosted_runner_info{runner_id,version,client_label} 常に 1。フリートの台帳と版のずれの検出に
claude_code_self_hosted_runner_capacity 設定した --capacity
claude_code_self_hosted_runner_active_sessions 動いているセッション数
claude_code_self_hosted_runner_locked_account{email} ユーザーにロックされ、act.email を持つトークンが出たあとだけ出る(Claude Tag のエージェントでは出ない)。値はメールなので、ストアが広く読めるならスクレイプ時にラベルを落とすかハッシュする(metric_relabel_configs など)
claude_code_self_hosted_runner_last_poll_age_seconds 最後に成功した poll からの秒数。60を超えたら通知
claude_code_self_hosted_runner_poll_errors_total{error_kind} PollWork の失敗の累計(transport・timeout・5xx・429・4xx)。5つともプロセスの開始からある。rate(...[5m]) > 0 で通知
claude_code_self_hosted_runner_sessions_started_total{client_platform} 起動したセッションの子の数を出どころ別に(web_claude_ai・ios・android・desktop_app・claude_code_cli、送られなければ unknown)。Slack は claude_in_slack か claude-in-slack なので {client_platform=~"claude[-_]in[-_]slack"} で両方を拾う。フリート全体は sum()
claude_code_self_hosted_runner_sessions_completed_total{client_platform} きれいに終わったセッション(同じラベル)。範囲は単なる正常終了より広い(下の節)
claude_code_self_hosted_runner_sessions_failed_total{client_platform} 失敗で終わったセッション(同じラベル)
claude_code_self_hosted_runner_sessions_interrupted_total{client_platform} 結果ではなく運用上の理由でランナーが止めたセッション(同じラベル)
claude_code_self_hosted_runner_initializing_sessions 割り当てから子の init までの初期化中のセッション数
claude_code_self_hosted_runner_session_init_duration_seconds 初期化時間のヒストグラム
claude_code_self_hosted_runner_session_init_errors_total init の前に失敗したセッション(checkout フックの失敗・git の準備・トークン・init 前の子のクラッシュ)
claude_code_self_hosted_runner_session_start_hook_errors_total エラーを返した SessionStart フック(失敗した実行ごとに1)
claude_code_self_hosted_runner_session_idle_seconds{session_id,client_platform} アイドルになってからの秒数のゲージ。答えのない権限のプロンプトで止まったセッションを片づけるのに使える

オーケストレーターも /healthz と同じポートの GET /metrics で自分のシリーズを出します。

シリーズ 備考
claude_code_self_hosted_orchestrator_info{version,pool_id,orchestrator_uuid,hostname} 常に 1
claude_code_self_hosted_orchestrator_connected 直近の poll が成功なら 1、失敗なら種類を問わず 0
claude_code_self_hosted_orchestrator_last_poll_age_seconds 最後の poll の試行(成否を問わない)からの秒数。最後の成功から測るランナーの同名のものと違うので、失敗は connected と併せて見る。poll がフックを待つので、通知の閾値は60ではなく --hook-timeout に余裕を足した値(既定で約90秒)
claude_code_self_hosted_orchestrator_poll_errors_total{error_kind} PollSpawnHints の失敗の累計(transport・timeout・5xx・429・4xx)。rate(...[5m]) > 0 で通知
claude_code_self_hosted_orchestrator_queue_pending_sessions いま取れる起動の要求
claude_code_self_hosted_orchestrator_queue_backing_off_sessions 再試行できる失敗のあと、間を置いている要求
claude_code_self_hosted_orchestrator_queue_circuit_broken_sessions Owner が「Activity」タブで再試行するまで止まっている要求。0を超えたら通知
claude_code_self_hosted_orchestrator_pool_pending_sessions 環境でランナーを待つセッションの合計。環境全体の値でどのインスタンスでも同じなので、SUM ではなく MAX
claude_code_self_hosted_orchestrator_pool_active_sessions 環境で動いているランナーに割り当て済みのセッション数。同じく MAX
claude_code_self_hosted_orchestrator_spawn_hooks_total{result} spawn-runner フックの結果の累計(ok・retryable・non_retryable)。フックの呼び出しを数えるので sessions_started_total とは比べられない(容量が1超・ウォームプール・同じセッションの再起動でずれる)
claude_code_self_hosted_orchestrator_spawn_hook_duration_seconds フックの所要時間のヒストグラム
claude_code_self_hosted_orchestrator_warm_hints_dispatched_total プロセスの開始から送った待機の起動の要求の数
claude_code_self_hosted_orchestrator_session_queue_wait_seconds セッションがキューで待った秒数のヒストグラム(制御プレーンが送るタイムスタンプから)。p50/p99 の通知に。事前ウォームは含まない
claude_code_self_hosted_orchestrator_clock_skew_seconds ローカルからサーバーを引いた時計のずれ。診断用で、測ったあとに現れる
claude_code_self_hosted_orchestrator_scm_connector_connected SCM コネクタの WebSocket が開いていれば 1、接続中・待機中は 0。--scm-connector-host が無ければ出ない
claude_code_self_hosted_orchestrator_scm_connector_requests_forwarded_total 設定した SCM ホストへ中継した HTTP リクエストの累計。--scm-connector-host が無ければ出ない

自動スケールでは、やり方に合うシリーズを選び、ゲートしてからスケーラーへ渡します。

  • キューの深さで増減:queue_pending_sessions ではなく claude_code_self_hosted_orchestrator_pool_pending_sessions を HPA か KEDA へ渡す
  • 容量で増減:ランナーの active_sessions と capacity の比を使う
  • connected でゲート:切断したレプリカの古い値を混ぜないよう、claude_code_self_hosted_orchestrator_connected == 1 のインスタンスに絞る

注意点:

  • 全レプリカが切断すると、ゲートしたクエリは何も返さない。HPA は今のレプリカ数を保つが、KEDA の Prometheus スケーラーは既定の ignoreNullValues: "true" で空をゼロと読んで縮める。ScaledObject に ignoreNullValues: "false"(要るなら fallback の下限も)を設定する
  • Prometheus Operator の PodMonitor は、app.kubernetes.io/part-of: claude-code-self-hosted-runner のラベルと Kubernetes のレシピの名前付きの health ポートで、ランナーとオーケストレーターの両方を拾える

セッションの子のメトリクスを通す#

セッションは、自分の OpenTelemetry のメトリクスを持つ子プロセスで動きます。

  • --capacity が1より大きいとき:ランナーのホストに OTEL_METRICS_EXPORTER=prometheus、セッションの環境に CLAUDE_CODE_ENABLE_TELEMETRY=1(ラッパーか、継承されるランナーの環境で)を設定すると、各子のカウンターとゲージがランナーの /metrics にランナーのシリーズと並んで出る
  • 既定の --capacity 1:この書き換えは無く、子が自分の Prometheus のエンドポイントをポート 9464 で開く

セッションのライフサイクルのカウンターの意味#

sessions_started_total は子の起動時に増え、終了時に残りの3つのどれか1つが増えます。started から3つの合計を引くと、いま動いている子の数です。

カウンター 数えるもの
completed きれいに終わった:子が自分でコード 0 で終わった・接続中にセッションがアーカイブか削除された・ランナーが枠をきれいに返した(アイドルのタイムアウト・退役時刻・--kill-session-after-min の上限での解放、起動のタイムアウト、子の終了より先に気づいたサーバー側の割り当て解除)
failed 子が自分で非 0 のコードで終わった(クラッシュか、起動後の準備の失敗)
interrupted 成功でも故障でもない運用上の理由でランナーが子を止めた(ドレイン、または --kill-session-after-min の上限と SELF_HOSTED_RUNNER_MAX_LIFETIME_GRACE_MS の猶予のあとも残ったセッション。Kubernetes のローリング再起動の SIGTERM はドレインの例)
  • v2.1.260 より前は、上限に達したセッションをすべて止めて interrupted に数えた
  • post-session フックの CLAUDE_RUNNER_EXIT_REASON は、解放・起動のタイムアウト・割り当て解除を interrupted と報告するが、カウンターは completed に数える。フックの記録を sessions_completed_total とそのまま突き合わせると完了が少なく出るので、セッションごとの確認はフック、全体の割合はカウンターで見る

ワンショットの環境(--capacity 1 と既定の --drain-grace-sec 0)では、ランナーが1セッションの直後に終了します。completed・failed・interrupted は終了の直前にしか増えないので、15〜60秒ごとのスクレイプでは、シリーズが消える前にほぼ捉えられません。started は起動時に増えるので必ず見えますが、ワンショットでは完了数より「いま動いている数」に近くなります。目的ごとに次を使います。

目的 使うもの
スループット claude_code_self_hosted_orchestrator_spawn_hooks_total{result="ok"}。長く動くオーケストレーターのカウンターなので rate() が使える。数えるのはフックの呼び出しなので、事前ウォームや同じセッションの再起動でセッション数とずれる
利用率 sum(claude_code_self_hosted_runner_active_sessions) と sum(claude_code_self_hosted_runner_capacity) の比。どちらもランナーの寿命にかかわらず毎回のスクレイプで有効なゲージ
バックログ キューの深さは claude_code_self_hosted_orchestrator_pool_pending_sessions。claude_code_self_hosted_orchestrator_queue_circuit_broken_sessions は0を超えたら通知
失敗 claude_code_self_hosted_runner_sessions_failed_total(ベストエフォート)。起動後のクラッシュで増え、--drain-grace-sec が0より大きくランナーが長く動くなら rate() が使える。ワンショットでは取りこぼすので、見えた非 0 の値は調べる価値がある。起動前の失敗(checkout フック・git の準備・トークン)は session_init_errors_total にだけ出る
  • orchestrator_* は、オンデマンドのオーケストレーターを動かす環境にしか無い
  • ランナーが長く動く固定のフリートでは、スループットに sum(rate(claude_code_self_hosted_runner_sessions_started_total[5m])) を使う。ワンショットのフリートでは同じ問題があるので、キューにあるセッションの数で見る
  • バックログは管理画面の環境の「Activity」タブで見る(ランナーはキューの深さを出さない)
  • セッションごとの結果の記録には、突然の終了を除くすべての終了で動く post-session フックを使う

セッションの身元を確認する#

セルフホスト環境のセッションは、環境変数 CLAUDE_CODE_SESSION_ACCESS_TOKEN に署名つきの JSON Web Token(JWT)を受け取ります。

  • ふつうのベアラー資格情報として使える(curl -H "Authorization: Bearer $CLAUDE_CODE_SESSION_ACCESS_TOKEN" で社内のサービスを呼ぶなど)
  • セッションは社内で動くので Claude は社内のサービスを直接呼べる。受ける側は、自社の環境のセッションからの呼び出しか、誰(どのユーザーかサービス)が作ったセッションかを確かめたい
  • トークンは Anthropic が署名し、検証の鍵を公開の JWKS エンドポイントに出している。社内のサービスは鍵を取って署名を確かめ、クレームを読んで与える権限を決める

トークンが示すもの#

  • 示すこと:Anthropic が、特定の環境の特定のセッションに発行したこと。セッションの作られ方(組織のユーザーか、組織のサービスの ID か。Claude Tag のチャンネルのセッションは後者)
  • 示さないこと:どのプロセスが出したか。トークンはセッションの環境変数にあるので、Claude が動かすコードも、ツールや MCP サーバーも読んで出せる

受ける側が守ること:

  • aud を自社の環境 ID(管理ページに出る ccpool_...)と照合し、ほかの組織の環境向けのトークンを拒む
  • トークンから作る資格情報は、作成者ができること全部ではなく、1つのコーディングのセッションに要る範囲に絞る

トークンの形式#

値は sk-ant-cc- の接頭辞に標準の3部構成の JWT が続く形です。JWT ライブラリへ渡す前に接頭辞を外します。Anthropic ホストのクラウドセッションのトークンは sk-ant-si- で始まり別の鍵で署名されているので、sk-ant-cc- で始まらないものは拒みます。

text
sk-ant-cc-<base64url header>.<base64url payload>.<base64url signature>

署名のアルゴリズムは ES256(P-256 曲線の ECDSA と SHA-256)で、ヘッダーの kid が JWKS のどの鍵で署名したかを示します。

自社のサービスからトークンを検証する#

検証の鍵は、認証の要らない次のエンドポイントで公開されています。

text
https://api.anthropic.com/v1/code/.well-known/jwks.json
  • 応答は標準の JSON Web Key Set
  • 鍵は定期的に回り、古い鍵はそれで署名したトークンを検証できるようしばらく残る。1つの鍵に固定しない
  • Cache-Control: public, max-age=300 なので、キャッシュして5分ごとに取り直してよい

トークンごとの確認です。

段 確認
1. 接頭辞 sk-ant-cc- で始まらなければ拒み、接頭辞を外す
2. 署名 ヘッダーの kid に合う JWKS の鍵で ES256 の署名を確かめる。alg が ES256 でなければ拒む。キャッシュに無い kid なら、拒む前に1回 JWKS を取り直す(鍵の更新後の新しいトークンはキャッシュに無い鍵で署名される)
3. 発行者 iss がちょうど ccr でなければ拒む
4. 受け手(環境) aud は配列。自社の環境 ID(ccpool_...)を含まなければ拒む。ID は管理ページの環境の詳細と、トークンの ccr:pool_id に出る。これでほかの組織のトークンを弾ける
5. ロール ccr:role がちょうど session_worker でなければ拒む。同じ鍵で署名されるほかのトークン(環境シークレット・ランナートークン・ワークオーダー)はロールが違う
6. 有効期限 exp が過ぎていれば拒む。既定の寿命は4時間(最大8時間)。期限前にランナーが新しいトークンをセッションへ渡し、以降のサブプロセスはそれを継承するので、1つのセッションから別々の有効なトークンが何度か届きうる
7. ID の読み取り 作成者は act クレーム。act.sub は user:<id> の形の Anthropic のユーザー ID、act.email は記録があればメール。組織のサービスの ID が作ったセッション(Claude Tag のチャンネルを含む)は agent: の主体を持つので、ID のクレームの有無ではなく、act.sub が user: で始まるときだけユーザーのセッションとして扱う

Node.js の jose(JWKS の取得・キャッシュ・kid の選択をしてくれる)での例です。

typescript
import { createRemoteJWKSet, jwtVerify } from "jose";

const JWKS = createRemoteJWKSet(
  new URL("https://api.anthropic.com/v1/code/.well-known/jwks.json")
);

const PREFIX = "sk-ant-cc-";
const EXPECTED_POOL_ID = "ccpool_...";

export async function verifySessionToken(raw: string) {
  if (!raw.startsWith(PREFIX)) {
    throw new Error("not a self-hosted runner session token");
  }
  const jwt = raw.slice(PREFIX.length);

  const { payload } = await jwtVerify(jwt, JWKS, {
    issuer: "ccr",
    audience: EXPECTED_POOL_ID,
    algorithms: ["ES256"],
  });

  if (payload["ccr:role"] !== "session_worker") {
    throw new Error("token is not a session_worker token");
  }

  const act = payload.act as { email?: string; sub?: string };
  return {
    sessionId: payload["ccr:session_id"] as string,
    poolId: payload["ccr:pool_id"] as string,
    orgId: payload["ccr:org_id"] as string,
    creatorEmail: act?.email,
    creatorSub: act?.sub,
  };
}

Python なら PyJWT の PyJWKClient で同じことができます(jwt.decode に algorithms=["ES256"]・issuer="ccr"・audience=<環境 ID> を渡し、ccr:role が session_worker かを確かめる)。

セッションの中でトークンを検証する#

ラッパーは Claude が始まる前にセッションの中で動きます。JWT ライブラリの代わりに、ランナーのバイナリの self-hosted-runner decode-token を使えます。

  • トークンは引数・CLAUDE_CODE_SESSION_ACCESS_TOKEN・パイプの stdin の順に探す
  • 接頭辞を外し、JWKS で署名を、さらに有効期限を確かめ、クレームを JSON で出す
  • iss・aud・ccr:role は確かめないので、認可に使うなら出力から読んで自分で比べる

作成者の ID を、メール・act.sub(user:<id> か agent:<id>)の順で取り出す例です。

bash
"$CLAUDE_RUNNER_CLAUDE_BIN" self-hosted-runner decode-token | jq -re '.act.email // .act.sub'
  • CLAUDE_RUNNER_CLAUDE_BIN はランナー自身のバイナリの絶対パス。PATH で引いた claude ではなくこれを使い、ランナーと同じ版でデコードする
  • jq -r ではなく jq -re にする(-r だけだと、無いクレームが文字列 null になって 0 で終わり、悪い値が黙って流れる)
  • --no-verify は、JWKS に届かないオフラインの確認のときだけ使う

クレームの一覧#

  • ID は ccr:* の名前空間と act の連鎖から読む(平らな account_email・organization_uuid・account_uuid は後方互換の重複で、なくなりうる)
  • 組織のサービスの ID が作ったセッションは、act.sub が agent: で、act.email・ccr:account_id・account_email・account_uuid を持たない
  • 2つのメールのクレームはユーザーのセッションでも無いことがある(作成のリクエストの資格情報がメールを持つときだけ記録され、CLI から dispatch したセッションは両方無いことがある)。ID はメールではなく act.sub か ccr:account_id で引く
  • 表に無いクレームは無視する
クレーム 型 内容
iss 文字列 常に ccr
sub 文字列 ccr:session:<session_id>
aud 文字列の配列 常に anthropic-api を含み、セルフホストでは ccpool_... の環境 ID も含む。検証するのは環境 ID のほう
exp 数値 有効期限(Unix 時刻)。既定の寿命は4時間、最大8時間
iat 数値 発行時刻(Unix 時刻)
jti 文字列 トークンごとに一意の識別子
ccr:role 文字列 セッショントークンでは常に session_worker
ccr:session_id 文字列 セッション ID。sub の末尾と同じ
ccr:pool_id 文字列 自社の環境 ID。aud の値と同じ
ccr:org_id 文字列 自社の Anthropic の組織 ID
ccr:account_id 文字列 作成者のアカウント ID(act.sub から user: を除いた user_...)。spawn-runner フックの CLAUDE_RUNNER_ACCOUNT_ID や --lock-to-account の値と同じ文字列で比べられる
account_email 文字列 act.email の重複。act.email が無ければ無い
organization_uuid 文字列 自社の Anthropic の組織の UUID
account_uuid 文字列 作成者の Anthropic のアカウントの UUID
act オブジェクト RFC 8693 の委任の連鎖(下の表)

act の連鎖#

act は、セッションを作った ID から、ランナーを受け入れたシークレットの環境、そのシークレットを作った ID までの委任の経路を持ちます。いちばん外側が作成者なので、act.sub がそのまま作成者です。

経路 内容
act.sub 作成者の Anthropic のユーザー ID(user:<id>)。組織のサービスの ID が作ったセッション(Claude Tag のチャンネルなど)では agent:<id>
act.email 作成者のメール(作成時に記録されたとき)。必須にせず、act.sub で引く
act.attested_by 上流の ID プロバイダによる作成者の証明のための予約。ふだんは無いと考え、頼らない。act.sub をキーにし、アドレスが要るなら act.email があるときに読む
act.act セッションを起動したランナー。act.act.sub は ccr:runner:<runner_id>
act.act.act 環境。act.act.act.sub は ccr:pool:<pool_id>
act.act.act.act ランナーが登録に使った環境シークレットを作った ID。連鎖はここまで

導出する資格情報を絞る#

セッショントークンは作成者を示しますが、作成者本人のログインと同じに扱ってはいけません。

  • セッションの環境変数にあるので、Claude が動かすコードやツール・MCP サーバーも読んで出せる
  • 検証はオフラインなので、JWKS で通るトークンは、その後セッションに何があっても exp まで有効。Anthropic はセッショントークンの失効の一覧を出していない

社内の資格情報に替えるときは、1つのコーディングのセッションに要る範囲に絞ります。

  • 能力:作成者がほかで持つ管理の権限ではなく、コーディングに要るリソースの読み書きだけ
  • 寿命:トークンの exp 以下にする
  • 監査:作成者の ID と一緒に ccr:session_id と jti を記録し、行動をセッションまでたどれるようにする

作成者の ID は、トークンを検証しない2か所の環境変数にも出ます。

  • オーケストレーターの spawn-runner フック(CLAUDE_RUNNER_ACCOUNT_EMAIL・CLAUDE_RUNNER_ACCOUNT_ID):ワークオーダーから署名を検証せずに読むが、環境シークレットで認証したオーケストレーターと Anthropic の接続で届くので信頼できる
  • セッションの中のラッパー(CCR_SESSION_ACCOUNT_EMAIL):署名を検証せずに取り出した値。ラベル付けには使えるが、認可には使わない

マシンイメージの選択のようなオーケストレーター側の判断にはこれらの変数を、ランナーの環境を信用せず暗号で確かめたい下流のサービスには CLAUDE_CODE_SESSION_ACCESS_TOKEN を使います。

エンドツーエンドでテストする#

新しいランナーのイメージを本番の環境へ出す前に、テスト用の環境でセッションを丸ごとスクリプトから動かします。セッションを作る→返信を読む→続きを送る→その返信を読む、の一巡で、イメージ・git のアクセス・独自のツールを確かめる CI のスモークテストです。

  • 前提:環境とランナーを立ち上げ済みで、CI のジョブがテストスクリプトと同じホストでランナーを起動する
  • 返信はランナーに入れた Stop フックがローカルのファイルに書き、スクリプトはそこを読む。Anthropic の API を呼ぶのは2回の dispatch だけ

テストのランナーに取り込みのフックを入れる#

読み取りには Claude Code の Stop フックを使います。ターンの終わりに、フックは最後のアシスタントのメッセージを stdin の JSON の last_assistant_message で受け、$E2E_REPLY_DIR/<session_id>.txt に足します。入れ方は「セッションに作業を push させる」と同じで、設定のブロックをホストの ~/.claude/settings.json に足し、スクリプトを ~/.claude/hooks/e2e-stop-hook-capture.sh に置いて実行可能にします。

json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "timeout": 10,
            "command": "\"$CLAUDE_CONFIG_DIR/hooks/e2e-stop-hook-capture.sh\""
          }
        ]
      }
    ]
  }
}
sh
#!/bin/sh
# テスト用ランナーだけに入れる。jq が要る。
# ドライバーが待っているときだけ動き、ターンを決して失敗させない。
[ -n "${E2E_REPLY_DIR:-}" ] && [ -d "$E2E_REPLY_DIR" ] || exit 0

# CLAUDE_CODE_REMOTE_SESSION_ID は cse_... の形。dispatch の CLI が出す
# session_... の形と、同じ ID で接頭辞が違う。
sid=$(printf '%s' "${CLAUDE_CODE_REMOTE_SESSION_ID:-}" | sed 's/^cse_/session_/')
[ -n "$sid" ] || exit 0

# 最後のアシスタントのターンにテキストが無い(ツール使用だけのターン)と
# last_assistant_message は無い。`// empty` で、文字列 "null" ではなく
# 0バイトの書き込みになる。
jq -r '.last_assistant_message // empty' >> "$E2E_REPLY_DIR/$sid.txt" 2>/dev/null
exit 0
  • ランナーの起動前に入れる:~/.claude/ は起動時に1回だけ読まれるので、動いているランナーに足したフックは再起動後に効く
  • E2E_REPLY_DIR をランナーのプロセスへ export する:未設定かディレクトリが無いとフックは何もしない。ランナーを起動する場所(systemd のユニット・Pod の spec・CI のステップ)で設定し、下のスクリプトにも渡す
  • テスト環境のランナーにだけ入れる:E2E_REPLY_DIR がある限り全セッションの最後の返信をディスクに書く。使い捨ての CI のランナーなら問題ないが、誤って変数が入りうる本番のイメージには持ち込まない

テストのループを動かす#

スクリプトを動かすマシンには v2.1.224 以降が要ります(dispatch のフラグ --environment と --ref のため)。フックを入れてこのホストでランナーを起動したら、スクリプトは次を行います。

  1. claude -p "<prompt>" --environment <environment-id> --output-format json でテスト環境にセッションを作る。origin からリポジトリを自動で見つけるので git のチェックアウトの中で実行する。--ref <branch>(省略可)でローカルの HEAD ではなく指定の ref から始める。コマンドは session_id を含む JSON を1行出し、返信を待たずに終わる
  2. ターンのあとに Stop フックが書く $E2E_REPLY_DIR/<session_id>.txt に返信が出るのを待つ
  3. claude -p "<message>" --cloud <session_id> --output-format json で続きを送る
  4. 手順2と同じように続きの返信を待つ

フラグの決まり:

  • --environment は設定の remote.defaultEnvironmentId より優先する
  • --environment は --output-format stream-json に対応しない。セッションを再開・接続・事前設定するフラグ(--resume・--continue・--teleport・--session-id・--init-only)とも併用できない
  • --environment と一緒の --cloud は、セッション ID か URL を付けると拒否され、非対話の実行で説明を付けても拒否される。素の --cloud は無いものとして扱う
  • ターミナルからは、位置引数のプロンプトの代わりに --cloud の説明としてタスクを渡せる

次のスクリプトは、$CLAUDE_TEST_ENVIRONMENT_ID(テスト環境の ccpool_...)に対して一巡を動かし、各返信の目印の語句を確かめます。

bash
#!/usr/bin/env bash
set -euo pipefail

: "${CLAUDE_TEST_ENVIRONMENT_ID:?set CLAUDE_TEST_ENVIRONMENT_ID to a ccpool_... id served by a runner on this host}"
: "${E2E_REPLY_DIR:?set E2E_REPLY_DIR to the directory the Stop hook on your test runner writes to}"
: "${TEST_REPO_REF:=main}"

# $E2E_REPLY_DIR/<session_id>.txt が $2 を含むまで待ち、90秒で失敗する
await_reply() {
  local expect="$2" f="$E2E_REPLY_DIR/$1.txt"
  local deadline=$(($(date +%s) + 90))
  while :; do
    if [ -f "$f" ] && grep -qF -- "$expect" "$f"; then return; fi
    [ "$(date +%s)" -lt "$deadline" ] || { echo "FAIL: '$expect' not in $f within 90s" >&2; exit 1; }
    sleep 1
  done
}

# 1. テスト環境にセッションを作る(git のチェックアウトから実行する)
TURN1="e2e-probe-$(date +%s)-$$: say exactly 'ok: custom tools are reachable' and nothing else"
EXPECT1="ok: custom tools are reachable"
create_json=$(claude -p "$TURN1" --environment "$CLAUDE_TEST_ENVIRONMENT_ID" \
  --ref "$TEST_REPO_REF" --output-format json)
SESSION_ID=$(jq -er '.session_id' <<<"$create_json")

# 2. ターン1の返信を待つ
await_reply "$SESSION_ID" "$EXPECT1"

# 3. 続きを送る
TURN2="e2e-probe-followup-$(date +%s): say exactly 'ok: follow-up delivered' and nothing else"
EXPECT2="ok: follow-up delivered"
followup_json=$(claude -p "$TURN2" --cloud "$SESSION_ID" --output-format json)
jq -e '.ok == true' <<<"$followup_json" >/dev/null

# 4. ターン2の返信を待つ
await_reply "$SESSION_ID" "$EXPECT2"
echo "PASS: test-environment round-trip (session $SESSION_ID)"

TURN1/TURN2 のプロンプトと EXPECT1/EXPECT2 の目印は、自社の構成を試す内容に替えます(独自の MCP ツールを動かさせて出力を確かめるなど)。

別のインフラのテストランナー#

テストのランナーが CI のジョブとファイルシステムを共有できない場所(常設の Kubernetes のフリートなど)にあるなら、フックのファイル書き込みを、ドライバーが待ち受けるエンドポイントへの POST に替えます。

  • ランナーに E2E_REPLY_URL を設定し、フックの最後を jq -r '.last_assistant_message // empty' | curl -fsS -X POST --data-binary @- "$E2E_REPLY_URL/$sid" にする(E2E_REPLY_URL が無ければ何もしない点と sid の作り方は上と同じ)
  • ドライバー側では、POST を受けてテストが求めるまで返信を持っておくもの(CI のジョブの中の小さな HTTP リスナーや、運用中の webhook の受け手)を動かす
  • フックは自社のインフラで動くので、エンドポイントはランナーから届けば足りる

CI から認証する#

claude -p ... --environment と claude -p ... --cloud は、どちらも claude.ai の OAuth トークンで認証します。API キー(sk-ant-xxxxx)は使えません。CI でのトークンの用意は2通りです。

  • 常設の CI ホスト
    • 自動化専用のユーザーアカウントで、そのマシンで claude auth login を対話で1回実行する
    • 保存先は macOS ならキーチェーン、Linux と Windows は ~/.claude/.credentials.json(キーチェーンに書けない macOS、たとえばログインのキーチェーンがロックされた SSH のセッションでも ~/.claude/.credentials.json)
    • 短命なアクセストークンは CLI が呼び出しごとに更新するが、リフレッシュトークンは最初のログインから30日が上限なので、30日ごとにそのホストで claude auth login をやり直す
  • 使い捨ての CI ランナー
    • いまは長期の CI 用トークンが無い。クラウドセッションの操作のスコープ user:sessions:claude_code はサーバー側で30日が上限で、推論専用の1年のトークンを作る claude setup-token では足りない
    • 環境シークレットも使えない(ランナーの登録だけを許し、セッションの作成は許さない)
    • 保存したログインを持ち込むには CLAUDE_CODE_OAUTH_REFRESH_TOKEN と CLAUDE_CODE_OAUTH_SCOPES を設定し、claude auth login にブラウザなしで交換させる(同じ30日の上限)
    • 人のアカウントに縛られないマシン ID が要るなら、Anthropic のアカウント担当へ連絡する

専用のテスト環境を作る#

CI の実行ごとに新しい環境を使えるよう、環境をプログラムで作って消せます。CI のジョブが起動するランナーは、その新しい環境に登録します。

  • 作成と削除は管理ページと同じエンドポイントで、anthropic-beta: ccr-byoc-2025-07-29 のヘッダーが要る
  • $ADMIN_TOKEN は Owner のアカウントの claude.ai の OAuth アクセストークン。「CI から認証する」と同じく Owner で claude auth login し、Claude Code の保存先から今のトークンを読む
  • トークンは毎回読み直し(CLI が回すのでコピーを持たない)、stdin で渡す(curl の引数にもビルドのログにも残らない)

作成するときの注意:

  • 応答をそのまま出さない。pool_secret は環境にランナーを登録できる長命の資格情報なので、マスクした CI のシークレットに入れ、出力は環境 ID だけにする
  • トークンをプロセス一覧に出さない -H @- には curl 7.55 以降が要る(古い curl は @- を文字どおりのヘッダーとして扱い、認証なしで送ってしまう)
  • Owner が「Allow self-hosted environments」をオンにするまで、403 の permission_error(self-hosted runners are disabled by your organization's policy)で失敗する
bash
create=$(curl -fsS -X POST -H @- \
  -H "anthropic-beta: ccr-byoc-2025-07-29" -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{"name":"ci-test-environment"}' \
  https://api.anthropic.com/v1/code/runners/self-hosted/pools \
  <<<"Authorization: Bearer $ADMIN_TOKEN")
ENVIRONMENT_ID=$(jq -er .pool.pool_id <<<"$create")
ENVIRONMENT_SECRET=$(jq -er .pool_secret <<<"$create")

このホストで SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET=$ENVIRONMENT_SECRET、取り込みのフック、E2E_REPLY_DIR を設定してランナーを起動し、テストスクリプトを動かします。終わったら、次の実行がきれいな状態で始まるよう環境を消します。

bash
curl -fsS -X DELETE -H @- \
  -H "anthropic-beta: ccr-byoc-2025-07-29" -H "anthropic-version: 2023-06-01" \
  "https://api.anthropic.com/v1/code/runners/self-hosted/pools/$ENVIRONMENT_ID" \
  <<<"Authorization: Bearer $ADMIN_TOKEN"

公式ドキュメント(英語)

2026年10月5日時点の内容をもとに、日本語でまとめています。

ページの一覧