Claude Media
Claude Codeセルフホスト環境の本番運用 — default-denyとgit認証

Claude Codeセルフホスト環境の本番運用 — default-denyとgit認証

Claude Codeのセルフホスト環境を本番投入する手順を、ハードニングのチェックリスト・ネットワーク遮断・git認証3方式・Kubernetes/Docker Compose構成、Runnerの起動フラグの既定値までまとめます。

Claude Codeのセルフホスト環境は、最小構成ならクイックスタートの3ステップで動きます。ただし本番投入では話が変わります。Runnerは、その環境にセッションを送れる全員の代わりにモデルが指示したコードを自社インフラ上で実行します。組織の誰でも対象になり、Claude Tagのチャンネルセッションがルーティングされていれば、そのSlackワークスペースの参加者も含まれます。

この記事は、固めるべき設定を「どの設定がどの事故を防ぐか」の順に並べ直し、v2.1.287のclaude self-hosted-runner --helpで確かめた既定値も添えています。

本番接続前に固める8項目

RunnerはBashを既定で承認済みツールに含むため、Auto modeを使っていなくてもシェル経由の外向き通信は確認なしで実行されます。本番システムに接続する前に、次を1つずつ潰します。

  • セッションごとの使い捨てコンテナ: Runnerプロセスをプロセス終了時に破棄される新規コンテナ・VMで動かし、--capacity 1と既定の--drain-grace-sec 0で1コンテナ1セッションに固定する。--capacityを上げるかドレイン猶予の値を正にすると、同じロックされたオーナーの複数セッションが1コンテナに同居する。Runner再起動をまたいでファイルシステムを再利用しない構成が原則で、意図的なpre-warmed checkout構成だけが例外だが、その場合もオーナーをまたぐ再利用は行わない
  • 広範な認証情報をイメージに含めない: 長期のSSHキー・クラウド認証情報・広い権限のPersonal Access Tokenをイメージに焼き込まない。push・APIトークンのようなセッション中の認証情報はwrapper scriptからセッションごとに発行する。wrapper実行前の初回クローンにはcheckoutライフサイクルフックか--use-anthropic-git-proxyを使う
  • environment secretをセッション実行ホストから遠ざける: environment secretはRunnerを登録でき、そのEnvironmentにキューされたどのセッションも拾える強い権限を持つ。固定Runner群ではどのホストにも常駐し、セッションのコードから読める状態になる。オンデマンドRunnerならsecretはユーザーコードを一切実行しないオーケストレーターホストだけに留まる
  • default-deny方式のアウトバウンド遮断: RunnerとセッションコンテナのアウトバウンドをEnvironmentごとに自社のネットワーク境界で制限する(詳細は次節)
  • ホストIAMを最小権限に: Runnerホストに付くインスタンスプロファイルやノードのサービスアカウントは、Runner自身が必要とする権限だけに絞る。セッションがホストの権限を継承しないよう、認証情報はwrapper scriptから取得する構成にする
  • クラウドメタデータエンドポイントの遮断: サブネット単位のegressポリシーはリンクローカルのメタデータ通信を素通りさせるため、コンテナ自体で遮断する。IMDSv2をホップ制限1に設定、GKE Workload Identityならメタデータ隠蔽を有効化、169.254.169.254への明示的な拒否ルールをセッションコンテナのネットワーク名前空間に追加、のいずれか
  • Runnerごとのファイルシステム分離: 他のプロセスから読み書きできない専用の作業ディレクトリを各Runnerプロセスに割り当てる。--hooks-dir・wrapper script・ホストの~/.claude/はセッションから読み取り専用にする
  • --confine-repo-settingsガードを有効化: 既定のwarnは違反をログに残しつつセッションを起動し、enforceはセッションを拒否し、offはスキャン自体を無効化する。スキャン対象は、セッションのワークスペース外を指すadditionalDirectoriesエントリ・permissions.allow内のEdit/Write/NotebookEditルール・sandbox.filesystem.allowWriteまたはallowReadエントリ、空でないenvブロック、sandbox.enabled: falseのように運用側の方針を上書きする設定。リポジトリのhooksや.mcp.json、Bashルールはこのガードの対象外です

ディスパッチ自体にはEnvironment単位のアクセス制御が無い点も踏まえます。Anthropic組織のメンバーなら誰でも、どのEnvironmentにもセッションを送れます。OwnerがClaude TagチャンネルをそのEnvironmentにルーティングしていれば、Claude Tagのアクセス設定が許可する全員(既定では接続済みSlackワークスペースの全員、Claudeアカウントの有無を問わず)もチャンネルセッションを起動できます。--lock-to-accountはどのアカウントのセッションをそのホストが実行するかを絞るだけで、誰がEnvironmentへディスパッチできるかは絞りません。Ownerは、組織全体でAnthropicホスト環境をCloud environmentsページから非表示にもできます。

公式レシピのまま本番に置いてはいけない設定

公式のKubernetes・Docker Composeレシピは、どちらも--capacity 4で書かれています。1コンテナが同じオーナーの最大4セッションを抱える構成なので、上のチェックリストの先頭項目(1コンテナ1セッション)とは両立しません。本番では次のどちらかに寄せます。

  • レシピを--capacity 1にして、セッションごとにコンテナを作り直す
  • オンデマンドRunnerに切り替える。environment secretもオーケストレーターに閉じられる

Anthropic git proxyを足す場合も--capacity 1が前提です。Docker Composeのrestart: alwaysは同じコンテナを書き込み可能レイヤーごと再起動するため、評価用途までにとどめます。

v2.1.287の--helpで確かめた既定値

起動フラグの既定値は、本番の安全性に直結するものが多くあります。claude self-hosted-runner --helpの出力から、ハードニングに関わる行を抜き出します。

claude --version
claude self-hosted-runner --help | grep -E '^  --(capacity|remove-session-state|confine-repo-settings|health-port) '
2.1.287 (Claude Code)
  --capacity <n>              Max concurrent sessions (default: 1)
  --remove-session-state [bool]
  --confine-repo-settings <mode>
  --health-port <port>        Port for /healthz HTTP listener (default: 8080). 0 disables.

チェックリストの8項目に入らない既定値です。

フラグ既定値本番で効く理由
--remove-session-state既定値false本番で効く理由転写コピー・シェルスナップショット・デバッグログなどがディスクに残り、同じ--base-dirを使う後続セッションから読める
--confine-repo-settings既定値warn本番で効く理由不正な値は起動時に失敗側へ倒れる(enforceの打ち間違いでwarnに落ちることはない)
--trust-workspace既定値true本番で効く理由リポジトリ内の.claude/settings.jsonのpermissions.allowとadditionalDirectoriesが信頼される。falseにするとリポジトリ側の許可は破棄され、ホスト側のsettings.jsonで与える
--host-config-snapshot既定値disk本番で効く理由起動時の~/.claude相当のコピーが改ざんされると、セッションは失敗してRunnerの再起動が必要になる
--startup-timeout-min既定値15本番で効く理由起動時にMCP接続や--resumeの復元で固まったセッション枠を解放する

--remove-session-stateは特に見落としやすい項目です。転写のコピーが残る範囲は同一Unixユーザーで、--capacity 1でファイルシステムを使い捨てにする構成なら問題になりませんが、永続ボリュームを--base-dirに置く構成では有効化を検討する価値があります。--lock-to-accountで1ユーザーに固定したRunnerなら、永続ボリュームのディスクはそのアカウントにしか使われません。固定したRunnerはClaude Tagチャンネルセッションを拾いません。

--use-anthropic-git-proxyには強い警告があります。起動時と各セッションの前に、Runnerを動かすアカウントのHOMEにあるgit設定(~/.gitconfig、GIT_CONFIG_GLOBALの指す先、$XDG_CONFIG_HOME/gitディレクトリ全体)をバックアップなしで削除して置き換えます。個人のログインでは動かさず、専用アカウントかコンテナで使います。運用側のgit設定は/etc/gitconfigか--configure-gitに置きます。

ネットワークの外向き通信を制限する

RunnerとセッションのHTTPS接続先を、必要なホストだけに絞ります。常に必要なのは2つです。

ホストポート用途
api.anthropic.comポート443(SCM connectorのみWSS)用途コントロールプレーン・セッションストリーミング・モデル推論・JWKS鍵取得・コミット署名
自社のgitホスト(github.com等)ポート443または22用途クローン・push。--use-anthropic-git-proxy使用時は不要

構成によって要否が分かれるホストは次のとおりです。

ホスト必要になる条件
downloads.claude.ai必要になる条件ネイティブインストーラーでホストに導入・更新するとき。セッション実行時はAnthropic公式マーケットプレイスのプラグインを入れるときだけ
storage.googleapis.com必要になる条件/pluginに出るプラグインのインストール数とメタデータ
registry.npmjs.org必要になる条件プラグインのインストール、npx起動のMCPサーバー
code.claude.com / claude.com必要になる条件ドキュメント参照とWebFetch。遮断しても影響はドキュメント参照だけ
*.frame.claudeusercontent.com必要になる条件Artifactツールが使えるときだけ。CLAUDE_CODE_DISABLE_ARTIFACT=1で無効のまま固定できる
http-intake.logs.us5.datadoghq.com必要になる条件CLAUDE_CODE_BYOC_ENABLE_DATADOG=1のときだけ。セルフホストでは既定オフ
browser-intake-us5-datadoghq.com必要になる条件エラーレポートが有効なアカウントのとき。DISABLE_ERROR_REPORTING=1かDISABLE_TELEMETRY=1で抑止できる

statsig.anthropic.com・*.sentry.io・claude.ai・platform.claude.com・mcp-proxy.anthropic.comへの到達は不要です。古い企業向けチェックリストに残っていることがありますが、機能フラグの取得はapi.anthropic.com経由になり、Runnerはインタラクティブなwebサインインではなくenvironment secretで認証するためです。例外は2つのホスト側フローで、インストーラーはclaude.aiからinstall.shを取得し、対話的なclaude auth loginはclaude.ai・claude.com・platform.claude.comを経由します。どちらもegressを許可した別のホストから実行します。

セッションのコードはモデルが指示した内容で任意のホストへ接続を試みる可能性があります。default-deny方式の遮断は、その接続がどこに着地するかを境界側で制限する対策で、権限モードの設定に関係なく効きます。

認証付きegressプロキシを使う場合

一部の企業向けegressプロキシは、接続ごとにProxy-Authorizationヘッダーを要求します。HTTPS_PROXY/HTTP_PROXYは通常どおり設定した上で、--proxy-authorization-commandか--proxy-authorization-fileでヘッダー値の取得元を指定します(いずれもClaude Code v2.1.238以降)。ヘルプによると、指定するとRunnerは127.0.0.1で小さな転送プロキシを起動し、自分と全セッションをそこ経由にします。ALL_PROXYは参照されず、セッション側では消されます。

TLS検査プロキシが証明書を再署名する環境では、GIT_SSL_NO_VERIFYが効きません。Anthropic管理のgit経由のクローンは、この変数を設定しても証明書を検証します。gitが自社のCAを信頼するまでクローンは失敗します(v2.1.283以降)。Runnerホストのシステム証明書ストアに自社CAを入れるか、GIT_SSL_CAINFOにCAのPEMファイルを指定します。

--proxy-authorization-*を指定したとき、起動に失敗する条件が3つあります。

  • 両方のフラグを同時に指定した(片方のフラグともう片方の環境変数も同時指定として扱われる)
  • HTTPS_PROXYにもHTTP_PROXYにもhttp://かhttps://のURLが入っていない
  • オーケストレーターにこのフラグを渡した。ヘルプにもNot supported with the orchestrator subcommand yetとあり、フラグは起動される各Runner側へ渡す

トークンを取れない・30秒でタイムアウトする・空文字を返すときは、その接続に502 Bad Gatewayが返ります。コマンド方式なら、ホストで同じコマンドを手で実行して標準出力にヘッダー値全体が出るかを確かめるのが近道です。

git認証を設定する

Runnerはチェックアウト自体は管理しますが、git IDや認証情報は既定で設定しません。3つの選択肢があります。

方式必要なgitバージョン向く場面
Runnerに設定させる(--configure-git)必要なgitバージョン2.34以降(SSHコミット署名)向く場面Anthropicホスト環境と同じidentity・署名設定で揃えたい
イメージにgit設定を焼く(自前のbot identity)必要なgitバージョン制約なし(署名・proxy・push-outcome-on-releaseを使わなければ2.24で十分)向く場面自社のbot identityでコミットしたい
Anthropic git proxyを使う(--use-anthropic-git-proxy)必要なgitバージョン2.32以降向く場面イメージにgit認証情報を一切持たせたくない

--push-outcome-on-releaseで解放前にpushしたブランチから再開する場合は、gitは2.29以降が要ります。

Runnerに設定させる場合、user.name = Claudeとuser.email = noreply@anthropic.comがAnthropicホストセッションと同じ形で書き込まれ、Anthropicの署名サービス経由でSSH形式のコミット署名も設定されます。コミットには、セッション作成者を示すCo-authored-by:トレーラーも自動で付きます。イメージ側でcore.hooksPathを設定済みなら、Runnerはその設定を尊重してトレーラー用のhooksを入れず、[runner:git]の警告を出します。push用の認証情報はこのフラグでは設定されないため、別途イメージ側で用意します。

イメージにgit設定を焼く場合、Dockerfileでシステム全体に設定します。

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

長期間有効・広いスコープのpush用認証情報を共有イメージに焼き込まないことが重要です。イメージ内の認証情報は、そのイメージを実行する誰のセッションからも使えてしまいます。代わりに、セッション作成者のidentityをセッションJWTから復号し、wrapper scriptから短命でスコープの狭いトークンをセッションごとに発行します。--capacity 1のセッション単位使い捨てコンテナと組み合わせれば、認証情報がそのセッションより長生きしません。

Anthropic git proxyを使う場合、セッション自身の短命トークンで認証しながらAnthropicのgit proxy経由でクローンします。通常のユーザーセッションではセッション作成者のGitHub OAuthトークンを、bot・agentセッションでは組織のGitHub Appインストールトークンを使います。Runnerイメージにはgit認証情報が一切不要になります(SSHキーもcredential helperも.netrcも無し)。ただしproxy URLはセッション単位のため--capacity 1が必須で、自社のgitホストがAnthropicのインフラから到達可能である必要があります。社内限定のgitホストにはcheckoutライフサイクルフックを使います。

イメージ側で認証情報を用意する方式では、Runnerの組み込みクローンがGIT_TERMINAL_PROMPT=0・SSHのBatchMode=yes・GCM_INTERACTIVE=neverを設定します。プロンプトが出る認証方式は使えないため、認証情報はプロンプトなしで動く形にしておきます。

Runnerイメージをビルドする

Anthropicは事前ビルド済みのRunnerイメージを配布していません。claudeバイナリを土台に、リポジトリが必要とする言語ランタイム・コンパイラ・パッケージマネージャーを積み上げます。

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に差し替えます。Runnerに必要なのはClaude Code v2.1.224以降です。ビルド引数にはstableチャンネルの版番号を引けます。

docker build \
  --build-arg CLAUDE_CODE_VERSION="$(curl -fsSL https://downloads.claude.ai/claude-code-releases/stable)" \
  -t <your-registry>/claude-runner:latest .

再現性が要るなら版番号を直接渡して固定します。新モデルが新しい版を要求する場合は、stableをlatestに替えて引きます。各セッションの子プロセスはRunnerと同じバイナリで動き、セッション内の自動更新も切られているため、イメージに入れた版がそのまま全セッションの版になります。モデルが要求するClaude Codeのバージョンより古い版に固定すると、そのモデルへのリクエストはサーバーに拒否されます。

セッション1件あたりの目安は、メモリがrequest・limitとも4GiB、CPUはrequest 2・limit 4です。メモリの上限に達するとカーネルがコンテナ内のプロセスを落とし、セッションが作業の途中で終わりえます。CPUは上限でスロットリングされるだけなので、遅くなっても動き続けます。

resources:
  requests:
    cpu: "2"
    memory: 4Gi
  limits:
    cpu: "4"
    memory: 4Gi

ビルドとテストが負荷の大半を占めるため、代表的なビルドを実行してピークを実測し、そこに余裕を乗せます。--capacityを1より大きくする場合は、1セッション分の値にcapacityを掛けます。Runnerは--capacityでセッション数を絞るだけで、CPUとメモリはセッション間で自動分割しません。1セッションの取り分を縛るなら、wrapper scriptから上限を設定します。

KubernetesとDocker Composeで動かす

Runnerはポート8080(既定、--health-portで変更可)でGET /healthzを返すため、追加設定なしでKubernetesのprobeが使えます。ヘルプによると、このエンドポイントは常に200を返す生存確認です。ポーリングが止まったまま生きているRunnerの検出には、claude_code_self_hosted_runner_last_poll_age_secondsを別途監視し、60秒超でアラートを出します。

次のDeploymentは--capacity 4の評価用の形です。本番では前節のとおり1に変えます。

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
    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

CPU・メモリのresourcesブロックは載せていないため、前節の値にcapacityを掛けて自分で追加します。environment secretはローカルファイルからKubernetes Secretを作って渡します。

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

Docker Composeでは、同じ構成が次の形になります。

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

シャットダウンで作業を失わない猶予時間

ローリング再起動で作業が消える原因の多くは、猶予時間の不足です。terminationGracePeriodSeconds: 90が要点で、Kubernetesの既定30秒はRunnerのドレイン処理より短く、ドレインが終わる前にpodが止められます。

SIGTERMを受けると、Runnerは新規受付を止め、--drain-wait-sec(既定0)まで進行中のターンを待ち、セッションのプロセスツリーを終了してpost-sessionフックを実行します。ヘルプの既定値を足すと、全体の猶予は次のようになります。

数字

ドレイン経路の既定値(単位は秒)

  • --session-stop-grace-sec

    5

    Claudeプロセスが自分で終わるのを待つ時間

  • --post-session-hook-timeout-sec

    60

    post-sessionフックに与える時間

  • 固定オーバーヘッド

    15

    プロセス掃除の分

  • 合計(既定)

    80

    --push-outcome-on-release設定時は+30で110

既定値の合計(5+60+15=80)。Runnerは起動時にこの合計をログへ出す

--drain-wait-secの既定は0なので上の合計には入りません。ホストの停止方法に応じて、この合計以上の猶予を与えます。

  • SIGTERMの猶予期間で止める場合: KubernetesのterminationGracePeriodSecondsやDocker Composeのstop_grace_periodをこの合計以上にする
  • --retire-atで止める場合: retire時刻とホスト停止時刻の間に、通常のターンの長さ・バックグラウンドタスクの保持時間・このドレイン合計を足した余裕を持たせる。保持は最大60秒(--helpの記述)
  • --defer-shutdown-max-minで止める場合: 設定した分数に加え、post-release grace(既定75秒)とドレイン合計を足す

--defer-shutdown-max-minは、再起動中のRunnerに保持中のセッションを最初のシグナルで即座に手放させず、指定した分数だけ通常通り動かし続けさせるオプションです(v2.1.238以降)。猶予が足りないと、ヘルプの警告どおりセッションはpost-sessionフックもderegisterも通らずに強制終了されます。再キューまでの時間は、ヘルプでは約1分後、公式docsでは数分以内と書かれています。既定のドレインより悪い結果になるので、停止タイムアウトをn分+155秒以上に取れないときは使いません。2回目のシグナルはどの段階でも即座にドレインを強制し、3回目は強制終了です。

post-sessionフックの終了にも注意があります。ドレイン中の2回目のSIGTERMでRunnerが強制終了すると、実行中のフックは誰にも止められず、閉じたログパイプへの書き込みでSIGPIPEにより落ちることがあります。フックは自分の出力をファイルへ向けておきます。コンテナ構成ではRunnerがPID 1で、終了すればコンテナごと消えるため、強制終了はフックにとって致命的と考えます。

フリートのスケール方式

Runnerは1度に1人のオーナーしか担当しないため、最小レプリカ数は同時にアクティブになりうる利用者数とClaude Tag agentの数の合計です。--capacityはオーナー内の並列度を上げるだけで、オーナー間のスケールには寄与しません。

くらべる

固定フリートとオンデマンドRunner

構成が単純

固定フリート

一定数のRunnerレプリカを動かし、Prometheusメトリクスでスケールします。environment secretが全Runnerホストに置かれ、セッションのコードから読める状態になるため、疑わしい事象があればsecretをローテーションします。

secretを閉じ込めたい

オンデマンドRunner

claude self-hosted-runner orchestratorが、Runnerのいないままキューされたセッションをポーリングし、spawn-runnerフックで1セッションごとに1台起動します。secretはオーケストレーターホストに閉じます。

オーケストレーターの--helpには、フック側の契約が書かれています。spawn-runnerはkubectl create jobやEC2のRunInstancesのように、処理を非同期に投げてすぐ終わる形にします。終了コードは0が成功、1が再試行(バックオフ)、2以上が再試行しない失敗(サーキットブレーカー)で、標準エラーの末尾がnackのエラーとして返されます。既定値は--hook-timeoutが60秒、--hook-concurrencyが4、--expected-spawn-secondsが120秒で、HAで複数レプリカを置くときは--expected-spawn-secondsを全レプリカで揃えます。--min-idleで空きスロットを先に確保でき、既定は無効です。オーケストレーターの契約と監視はセルフホスト環境でオンデマンドRunnerを起動するで詳しく扱っています。

Runnerが1人のオーナーに固定される仕組み

Runnerが最初に受け取ったセッションのオーナーにロックされ、以降は--capacityまで同じオーナーのセッションだけを実行する、というのがセルフホスト環境全体の設計の軸です。ユーザーが始めたセッションはそのユーザーのアカウントがオーナーに、Claude Tagチャンネルセッションはユーザーアカウントが紐付かないためClaude Tag agent自体がオーナーになります。

同じEnvironmentのRunnerは、--base-dirと--capacityを揃えます。Runnerが落ちると別のRunnerがセッションを引き継ぎ、--capacity 1なら--base-dir直下、1より大きければセッション単位のワークツリーにチェックアウトします。値がRunnerごとに違うと作業ディレクトリが変わり、エージェントが記録した絶対パスが指す先が消えます。

本番で踏みやすい制限

  • connectorトラフィックは自社ネットワークの外に出る: GitHub・Slack・Linear等のclaude.aiコネクタは、RunnerからではなくAnthropic自身のインフラから呼び出されるため、セルフホストセッションでコネクタを使うとそのトラフィックはapi.anthropic.com経由になります。自社側に留めたい場合は、同等のツールをRunnerイメージ上のローカルMCPサーバーとして動かします。コネクタを使わせたくない場合はallowedMcpServers/deniedMcpServersでフィルタします。URLベースの許可リストを併用するなら、コネクタ用にhttps://api.anthropic.com/v2/ccr-sessions/*・https://api.anthropic.com/v1/code/sessions/*・https://api.anthropic.com/v1/code/mcp/*を追加します。許可リストを入れた側でコネクタも一緒に塞がれる、という落とし穴です
  • 一部のセッションはアイドル判定されない: バックグラウンドタスクを保持したまま終わらないセッションや、実行中のツール呼び出しから承認待ちのセッションは--release-idle-session-minで解放されません。そのため--kill-session-after-minを必ず併用してハードな上限にします
  • 再開したセッションは未pushの作業を失う: アイドルタイムアウトやRunner再起動でセッションが解放されユーザーが次のメッセージを送ると、新しいRunnerが開始ブランチから改めてクローンするため、pushしていなかった作業は失われます。--push-outcome-on-releaseを設定すると、解放前にRunnerがベストエフォートでoutcomeブランチをpushし、再開セッションがそこから始まります。コミットしていない変更は戻りません。有効化の前に、送信元リモートのclaude/*参照へのpush権限を絞ります(誰がpushしたか検証せずに取得するため)。checkoutフックで取ったリポジトリはこのpushの対象外なので、post-sessionフックでスナップショットを取ります
  • セッション途中で追加したリポジトリはクローンに失敗することがある: --use-anthropic-git-proxyを使わないRunnerでは、ホスト上のどの認証情報でもそのリポジトリを読めないと、gitの認証エラーで失敗します。作成時に必要なリポジトリをすべて選んでおきます
  • 未接続のコネクタは一覧に出ない: claude.aiの設定で接続していないコネクタはセルフホストセッションに表示されず、接続を促されもしません。追加したコネクタは、実行中のセッションでは使えないため、新しいセッションを始めます

--kill-session-after-minはv2.1.260以降のRunnerで挙動が変わっています。上限に達したセッションを即座に終了せず、既定15分の猶予(SELF_HOSTED_RUNNER_MAX_LIFETIME_GRACE_MSで変更可)の中で次の順に扱います。

  1. ユーザーの入力待ちなら、そのまま解放する。ターンが終わりバックグラウンドタスクだけが残るなら、最大60秒待って解放する
  2. ターン実行中なら、ターンが終わるか次に入力待ちになるまで待ってから解放する
  3. 猶予が尽きてもまだRunnerにいれば終了させ、実行中のターンの作業は失われる

解放されたセッションは新規クローンから再開するため、pushしていない作業はどちらでも消えます。v2.1.260より前は、上限で一律に終了していました。上限は想定する最長セッションより長く取り、たとえば8時間なら--kill-session-after-min 480です。

起動に失敗するRunnerを切り分ける

ガイド付き診断には、Runnerホストでdoctorサブコマンドを実行します。v2.1.287のヘルプでは、認証の連鎖・ネットワーク・ライフサイクル・セッション実行・キュー・バージョン・可観測性の判断木をたどり、/healthz・Runnerログ・管理APIから証拠を集めて、伏せ字済みの診断バンドルを作る対話型ウィザードと説明されています。

claude self-hosted-runner doctor

claude auth loginでサインイン済みなら、EnvironmentとそのRunner・キュー済みセッションまで踏み込めます。APIキーで動かしているホストでは、ローカルのヘルスエンドポイントとメトリクスとRunnerのログに限られ、ログを読めるのは--log-file付きで起動したときだけです。

Runnerが起動直後に終了し続けるときは、再起動を速めても直りません。次の順に原因を絞ります。

手順

起動直後に落ちるRunnerの確認順

  1. 1

    最後の行を読む

    標準出力・標準エラー・コンテナログ・--log-fileから、[runner:fatal]かerror:で始まる最後の行を探します。フラグのパース失敗・secretの読み取り失敗・ベースディレクトリの作成失敗はerror:で出て、ログファイルが開く前のためコンテナログにしか残りません。行がまったく無いなら、メモリ上限などでホストがプロセスを落とした可能性があります。

  2. 2

    フラグの組み合わせを疑う

    --use-anthropic-git-proxyと--capacityが1より大きい組み合わせは、[runner:fatal] --use-anthropic-git-proxy requires --capacity 1で終了します。--configure-gitとgit proxyは、PATH上のgitと書き込み可能な~/.gitconfigも要ります。

  3. 3

    手で動かして差を見る

    手元では起動するのにsupervisorの下で落ちるなら、ユーザー・ホームディレクトリ・PATH・メモリ上限を比べます。secretを失効させた、または打ち間違えたときは、RegisterRunner auth failedを含む行が出ます。

  4. 4

    管理画面の見え方を鵜呑みにしない

    登録後のステップで落ちると、プロセスは終了しているのに、Cloud environmentsページが数分間Healthyのままになることがあります。キューが動かないのに健全に見えるなら、EnvironmentのActivityタブで新しいRunnerが現れ続けていないか確認します。

再起動の間隔は、supervisorごとに挙動が違います。

  • Kubernetes: Deploymentに変更は要りません。kubeletが再起動の待ち時間を段階的に延ばします。ただし短時間で終わった正常終了にも同じ待ちが掛かるため、頻繁にドレインするRunnerはCrashLoopBackOffにも見えます。kubectl logs --previous -n claude-runners deploy/claude-runnerで直前の実行の出力を読んで判断します
  • Docker Compose: restart: alwaysでも、繰り返し終了するコンテナの再起動間隔は上限まで延びます。docker inspect --format '{{.RestartCount}}' <container>の数が増え続けるなら、Dockerが再起動し続けています
  • systemd: RestartSecは毎回同じ値で延びません。既定では10秒に5回起動すると再起動を止め、手動で起動し直すまで止まったままになります
  • 自前のループ: 5秒から始め、1分以内に終わった実行のたびに倍にして5分で頭打ち、1分以上動いたら5秒へ戻す

オンデマンドRunnerは作業指示が1回限りなので、再起動しません。

起動後の問題は次の症状で見ます。

症状見る場所
管理画面にRunnerが現れない見る場所api.anthropic.comへのHTTPS到達性、secretの有効性、ホストの時刻(実時間との5分以上のズレは認証失敗の原因)。[runner:fatal]に理由が出る
cannot create or write to base directoryで終了する見る場所--base-dir(既定/workspace)に書けない。所有権を直すか書き込める場所を指定する。非rootのRunnerではディレクトリを先に作る。v2.1.225より前は起動時にこの検査が無く、セッション拾得後に失敗していた
セッションがキューに残る見る場所オンラインのRunnerがすべて別のオーナーにロックされている。claude_code_self_hosted_runner_locked_accountメトリクスか[runner:health]ログのlocked_accountで確認する。Claude Tag agentのセッションにはメールのクレームが付かず、ログはlocked_account=yesとしか出ない
拾得直後にセッションが失敗する見る場所claude.ai/codeでエラーを見る。原因はgit認証情報の不足とビルドツールの未導入が多い
ターンが401で失敗する見る場所wrapperがセッションのstdinを切っていると、約30分後から全呼び出しが失敗する。v2.1.274より前は、取得失敗後の再試行も数回で止まっていた
起動に数分かかる見る場所初回クローンが支配的。claude_code_self_hosted_runner_session_init_duration_secondsで確認し、pre-warmed checkoutかCLAUDE_RUNNER_FETCH_DEPTHで縮める
応答が不正と警告される見る場所rejecting the malformed poll responseが出るなら、途中のプロキシやキャプティブポータルが独自のページを返している。v2.1.246より前は空のキューとして読まれ、稼働中のセッションが終わることがあった
読み取り専用リポジトリが消える見る場所refusalを受けたリポジトリはスキップされ、could not access context sourceの警告が出る。push先のリポジトリや403、ネットワーク障害はセッション起動を失敗させる。v2.1.274より前は、これらの拒否もすべて失敗扱いだった
podがドレイン途中で止まる見る場所terminationGracePeriodSecondsを、Runnerが起動時に出す合計以上にする

まとめ

猶予時間は停止方法ごとに必要な長さが違うため、SIGTERM・--retire-at・--defer-shutdown-max-minのどれで止めるかを先に決め、それに合わせて割り当てます。

ネットワーク遮断の設計はサンドボックス実行の考え方と同じ流れにあり、Claude Codeのサンドボックス設計やClaude CodeをDevContainerで安全に動かす完全実装も同じ「モデルが指示したコードをどこまで信用するか」という論点を扱っています。ライフサイクルフックの書き方全般はClaude Code Hooks完全ガイドを参照してください。

この記事を共有:XはてブLinkedIn