Claude Codeセルフホスト環境をラッパースクリプトとフックで拡張する
Claude Codeセルフホスト環境のセッションは、ラッパースクリプトとcheckout/post-session/commandの3つのフックで挙動を差し替えられます。環境変数と設定手順を確認します。
Claude Codeセルフホスト環境のRunnerは、設定なしでもリポジトリのクローン・Claude Codeの起動・後片付けを自動でこなします。ただし認証情報の発行元を変えたい、クローン処理そのものを差し替えたい、未コミットの作業をセッション終了時に必ず退避したいといった要件は、既定の動きだけでは満たせません。Runnerにはこれを埋める拡張ポイントが4つあります。セッションの実行そのものを乗っ取るラッパースクリプトと、パイプラインの特定の段階だけを差し替える3種類のライフサイクルフック(checkout / post-session / command)です。クイックスタートで最小構成のRunnerを動かした後、この4つをどう使い分けるかを確認します。
Claude Codeセルフホスト環境で拡張できる4つのポイント
ラッパースクリプトとライフサイクルフックは、いずれもRunnerホスト上で実行される実行可能ファイルです。対応OSはLinuxとmacOSで、本記事のサンプルはPOSIXシェルを前提にしています。
前提として、動作するRunnerがすでに1台あることを確認してください。設定方法は2通りです。フックのディレクトリを--hooks-dir <path>(環境変数SELF_HOSTED_RUNNER_HOOKS_DIR)で指定するか、ラッパースクリプトを--exec-path(環境変数SELF_HOSTED_RUNNER_EXEC_PATH)で指定します。フックディレクトリの中に決まった名前(checkout / post-session / command)の実行可能ファイルを置くと、存在するものだけが呼ばれます。置かなかったフックは組み込みの既定動作にフォールバックするため、必要なものだけ書けば済みます。
なお、ここで扱う「ライフサイクルフック」はClaude Code Hooksとは別物です。Claude Code Hooksはセッションの内側、モデルの応答やツール呼び出しに反応して動きます。ライフサイクルフックはセッションの外側、Runnerがセッションを起動・終了する過程に反応して動きます。同じ「フック」という語ですが、実行される場所も渡される環境変数もまったく別です。
ラッパースクリプトでセッションごとの認証情報を発行する
ラッパースクリプトは、Runnerが本来Claude Code本体を起動する場所に割り込ませる実行可能ファイルです。セッションごとに使い捨ての認証情報を発行する、環境固有のシークレットをエクスポートする、言語ツールチェーンを準備するといった、Runner単体では済ませられないセットアップに使います。
claude self-hosted-runner --environment-secret-file /etc/claude/environment-secret \
--exec-path /etc/claude/session-wrapper.shRunnerはラッパーの環境に複数の変数を渡します。中でも把握しておく価値が高いのは次の5つです。
| 変数 | 内容 |
|---|---|
CLAUDE_CODE_SESSION_ACCESS_TOKEN | 内容sk-ant-cc-で始まるセッションJWT。actクレームがセッション作成者を示します |
CLAUDE_RUNNER_CLAUDE_BIN | 内容Runner自身のClaude Codeバイナリへの絶対パス。exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@"で締めくくり、PATH解決のclaudeではなくこのバイナリに処理を渡します |
CCR_SESSION_ACCOUNT_EMAIL | 内容セッション作成者のメールアドレスを、署名検証なしでRunnerが事前抽出した値。コミットのトレーラーへの記載などラベリング用途に留め、認可判断には使いません |
CLAUDE_CODE_OAUTH_TOKEN | 内容モデル推論とファイルアップロードにのみ有効な、寿命約30分の短命OAuthトークン。ログに残さず、ディスクに書かず、コンテナ外へ転送しません |
CLAUDE_SESSION_INGRESS_TOKEN_FILE | 内容常に最新のセッションJWTを保持するファイルへの絶対パス。セッションに添付されたファイルをダウンロードする際の認証に使われます。execなら自動的に引き継がれますが、ラッパーが子プロセスの環境を作り直す方式だと明示的に引き継がないと添付ファイルのダウンロードがエラーなく壊れます |
セッション作成者ごとに認証情報を出し分けたい場合は、decode-tokenサブコマンドでJWTのクレームを読みます。次の例は、人間が作成したセッションだけを対象に、Anthropicのユーザーidから短命のAWS認証情報を発行してからClaude Codeを起動します。
#!/bin/bash
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" "$@"組織のサービスアイデンティティが作るセッションはact.subがuser:ではなくagent:で始まります。botやClaude Tagチャンネルのセッションが該当し、このスクリプトはそれらを拒否します。jq -rではなくjq -reを使うのがポイントです。-rだけだとクレームが無いとき文字列nullがそのまま返り、認可チェックを素通りしてしまいます。
checkoutフックでクローン処理を差し替える
checkoutフックは、リポジトリごとに1回、Runner組み込みのクローン・フェッチ処理の代わりに実行されます。読み取り専用ミラー経由でクローンする、アーカイブから作業ツリーを復元する、セッションごとのgit認証を適用するといった用途に使います。
| 変数 | 内容 |
|---|---|
CLAUDE_RUNNER_REPO_URL | 内容クローン対象のリポジトリURL |
CLAUDE_RUNNER_REPO_REF | 内容チェックアウトするブランチ・タグ・コミットSHA。空ならデフォルトブランチ |
CLAUDE_RUNNER_CHECKOUT_PATH | 内容作業ツリーを残す絶対パス |
CLAUDE_CODE_SESSION_ACCESS_TOKEN | 内容セッションスコープのAPI呼び出し用トークン |
スクリプトはCLAUDE_RUNNER_CHECKOUT_PATHに、要求されたリビジョンでチェックアウト済みの作業ツリーを残す必要があります。detached HEADで構いません。Runnerがその後、セッションの作業ブランチを積み上げます。RunnerはPerforceや展開済みtarballのようなgit以外のソースを想定していないため、後からパスに.gitが存在するかを検証します。git以外のソースを使う場合は、Runnerの環境にCLAUDE_RUNNER_SKIP_GIT_VERIFY=1を設定してこの検証を止めます。
注意点が2つあります。1つは、Runnerがgit認証情報をこのフックに渡さない点です。CLAUDE_CODE_SESSION_ACCESS_TOKENをJWTライブラリでJWKSエンドポイントに対して検証し、得られた作成者の身元からクローン用の短命認証情報を発行するのが基本形です。もう1つは、checkoutフックの環境にはCLAUDE_RUNNER_CLAUDE_BINが設定されないため、decode-tokenサブコマンドが使えない点です。ホストにすでにあるSSHエージェントやcredential helper、.netrcにフォールバックする選択肢もあります。
フックが非ゼロで終了した場合、または成功終了しても使えるチェックアウトを残さなかった場合の挙動は、リポジトリの性格で変わります。
| リポジトリの性格 | 挙動 |
|---|---|
| セッションが結果をpushする対象 | 挙動セッションを失敗させ、非ゼロ終了時はスクリプトのstderr末尾をユーザーに表示 |
| セッションが読み取るだけの追加リポジトリ | 挙動[runner:warn]ログを残し、Skippedステップをセッションに投稿して残りのリポジトリで継続。リポジトリが1つも残らないならセッションを失敗させる |
v2.1.228より前は、リポジトリの性格を問わずフック失敗でセッションを失敗させていました。読み取り専用リポジトリをフックが提供できないだけで、Runnerを移るたびにセッションが失敗し続けていたということです。
post-sessionフックで未コミットの作業を退避する
post-sessionフックは、Claude Codeの子プロセスが終了した後・Runnerが作業領域を片付ける前に、セッションごとに1回実行されます。未コミットの作業を保存できる唯一のタイミングです。--capacityが1を超える構成ではフック終了直後にセッションごとのワークツリーが削除されます。--capacity 1では次のセッション開始時に使い回すクローンがハードリセットされます。どちらの経路でも、未コミットの変更はフックが動く間しか生き残りません。ただしRunner自体がVMのプリエンプションや電源断で突然停止した場合は、このフックは発火しません。突然停止でも退避を保証したいなら、セッション内のClaude Code PostToolUseフックで定期的にスナップショットを取る運用を併用してください。
| 変数 | 内容 |
|---|---|
CLAUDE_RUNNER_EXIT_REASON | 内容セッションが終了した理由 |
CLAUDE_RUNNER_WORKSPACE_PATHS | 内容セッションの作業ツリーの絶対パス(コロン区切り) |
CLAUDE_RUNNER_SESSION_ID | 内容session_...形式のセッションID |
CLAUDE_RUNNER_EXIT_REASONは4つの値のいずれかです。
| 値 | 意味 |
|---|---|
completed | 意味クリーンな終了。子プロセス接続中にセッションがアーカイブ・削除された場合も含む |
failed | 意味子プロセスのクラッシュ、またはスポーン後のセットアップ失敗 |
interrupted | 意味アイドルによる解放・起動タイムアウト・サーバーの割り当て解除・ドレイン・ウォッチドッグによるkill |
abandoned | 意味別のRunnerがセッションを引き取った場合用に予約されている値で、この経路でフックが発火することはない |
次の例は、未コミットの変更をレスキューブランチにpushします。
#!/usr/bin/env bash
set -u
IFS=':'
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-c core.fsmonitor=falseや-c core.hooksPath=/dev/nullを付けているのは安全のためです。セッションが.git/configに仕込んだ設定が、フックの権限でコードを実行するのを防ぎます。フックの終了ステータスはセッションの結果に影響しません。失敗はログに残るだけで無視されます。Runnerはセッション終了のたびに(Runnerのシャットダウン時も含めて)--post-session-hook-timeout-sec(既定60秒)まで待ちます。
pushに使う認証情報は、Runnerホストの環境にすでにあるものがそのまま使われます。イメージに長期認証情報を焼き込まない構成や、組み込みクローンがAnthropicのgitプロキシを経由する構成では使える認証情報がありません。その場合はフック内でCLAUDE_CODE_SESSION_ACCESS_TOKENを検証し、そこから短命のpush用認証情報を発行してから使います。
v2.1.236以降のRunnerでは、セッションが解放される状況によって、このフックが完了する前に別のRunnerへ再開できるかどうかが変わります。ターンの合間のアイドルや起動タイムアウトでは、Runnerが子プロセスを止めてこのフックを完了させてからセッションを解放します。フック実行中に届いたユーザーメッセージは、フックが終わるまで別のRunnerで再開できません。一方、権限確認プロンプトなどユーザーの応答待ちで解放される場合は、Runnerが先にセッションを解放してからこのフックを実行するため、フック実行中でも別のRunnerで再開できます。v2.1.236より前は、どちらの経路でも先にセッションを解放してからこのフックを実行していました。
commandフックと--exec-pathの使い分け
commandフックは、checkoutの後・セッションごとに1回、Runner組み込みの子プロセス起動処理の代わりに実行されます。受け取る環境変数はラッパースクリプトと同じで、同様に"$CLAUDE_RUNNER_CLAUDE_BIN"へexecで処理を渡します。カスタマイズを1つのフックディレクトリにまとめたいならcommandフック、ラッパーを別の場所に置きたいなら--exec-pathを使います。両方指定した場合は--exec-pathが優先され、commandフックは無視されます。
PATH解決のclaudeではなく、必ずRunner自身のバイナリをexecしてください。そうしないと本番運用で説明したバージョン固定が機能しなくなります。
4つの拡張ポイントの使い分け早見表
| 拡張ポイント | 実行タイミング | 差し替える対象 | 向いている用途 |
|---|---|---|---|
| ラッパースクリプト | 実行タイミングセッションごとに1回、子プロセス起動時 | 差し替える対象Claude Code本体の起動そのもの | 向いている用途セッションごとの認証情報発行・ツールチェーン準備 |
checkoutフック | 実行タイミングリポジトリごとに1回 | 差し替える対象クローン・フェッチ処理 | 向いている用途ミラー経由のクローン・独自git認証・非gitソース |
post-sessionフック | 実行タイミングセッションごとに1回、終了直後 | 差し替える対象後片付け前の退避処理 | 向いている用途未コミット作業の退避・ログのアーカイブ |
commandフック | 実行タイミングセッションごとに1回、checkout後 | 差し替える対象子プロセス起動処理 | 向いている用途ラッパーと同じ用途を1つのフックディレクトリに集約したいとき |
よくあるつまずき
- 標準入力を切ってしまう: ラッパーが子プロセスを裸の
&でバックグラウンド化すると、約30分後に全API呼び出しが401で失敗し始めます。原因がトークン期限切れに見えて気づきにくい典型例です - checkoutフックで
decode-tokenを使おうとする: このフックの環境にはCLAUDE_RUNNER_CLAUDE_BINが設定されないため、decode-tokenサブコマンドは使えません。JWTライブラリで直接検証するか、ホストの既存git認証にフォールバックします - フックディレクトリを書き込み可能なままにする: フックはRunnerの権限で動き、セッションの子プロセスも同じUIDを共有します。フックディレクトリを読み取り専用でマウントするかイメージに焼き込まないと、セッション側のコードがフックを書き換えられます
jq -rでクレームの欠落を見逃す:jq -rはクレームが無いとき文字列nullをそのまま返して正常終了します。認可判断に使う抽出は必ずjq -reにして、欠落を非ゼロ終了で検知します--exec-pathとcommandフックを両方書いて意図しない方が動く: 両方設定すると--exec-pathが勝ちます。カスタマイズを一元管理したいなら、どちらか片方に統一します
まとめ
Claude Codeセルフホスト環境のセッションは、ラッパースクリプトと3種類のライフサイクルフックで拡張できます。セッションの起動そのものを差し替えるならラッパースクリプトです。クローン処理だけを差し替えるならcheckoutフック、終了時に未コミットの作業を退避するならpost-sessionフックを選びます。フックディレクトリにまとめて管理したいならcommandフックです。どの拡張ポイントも認証情報の発行にはセッションJWTの検証が絡むため、実装する前にセッション身元をJWTで検証するを確認してください。