ゲートウェイログインは、共有クライアントトークンを 1 つ配布する代わりに、Claude Code のユーザーごとにローテーションする独自の OAuth セッションを与えます。これはオプトインのサーフェスです。[server.gateway] がなければ、OAuth ルートもデバイス承認ルートも存在しません。
1. ログインサーフェスを設定する
32 バイト以上の署名シークレットと、カンマ区切りの email:secret 形式の承認ユーザーリストを作成します。どちらも shunt.toml ではなく shunt の環境変数に置いてください。
export SHUNT_GATEWAY_JWT_SECRET="$(openssl rand -base64 48)"
export SHUNT_GATEWAY_USERS='alice@example.com:<unique-secret>,bob@example.com:<unique-secret>'Claude Code とユーザーのブラウザーから到達できる公開 URL を追加し、[server.gateway.session] にシークレットを設定します。
[server.gateway]
public_url = "https://gateway.example.com"
users_env = "SHUNT_GATEWAY_USERS" # default
trust_forwarded_for = false # default
# state_path = "~/.shunt/gateway-sessions.json" # default; "" = memory-only sessions
[server.gateway.session]
jwt_secret = "${SHUNT_GATEWAY_JWT_SECRET}"
ttl_hours = 1 # defaultpublic_url が素の HTTPS オリジンでない場合(http はループバックでのみ許可されます)、token TTL が 0 の場合、署名シークレットが 32 バイト未満の場合、あるいは有効なユーザーリストと有効な外部 IdP のどちらも設定されていない場合、起動はフェイルクローズします。静的ユーザーのシークレットには : を含められます — 最初のコロンだけがメールアドレスとシークレットを区切るためです。
deprecated な jwt_secret_env(env 変数名、デフォルト SHUNT_GATEWAY_JWT_SECRET)と token_ttl_seconds(デフォルト 3600)は、単独で使う限り引き続き完全にサポートされます。token_ttl_seconds は今も 1 時間未満の寿命を指定できる唯一の方法です。deprecated なキーとその session.* 置き換えを併用すると起動が失敗します(jwt_secret_env と session.jwt_secret、または token_ttl_seconds と session.ttl_hours)。shunt は、deprecated なキーが設定ファイルであれ SHUNT_* 環境変数 override であれ明示的に設定されるたびに deprecation 警告を記録し、そのキー自体が一切設定されていない場合にのみ警告なしのままです(jwt_secret_env を設定せず SHUNT_GATEWAY_JWT_SECRET env 変数に secret の値だけを入れておく設定は、引き続き警告しません)。優先順位の全体像と secret のローテーション手順は設定リファレンスを参照してください。
代わりに Google OIDC を使う
Google Cloud で OAuth ウェブクライアントを作成し、承認済みリダイレクト URI に次を正確に設定します。
https://gateway.example.com/device/callbackそのシークレットをゲートウェイの環境変数に置き、issuer と必須の許可リストを設定します。
export SHUNT_GATEWAY_OIDC_SECRET='<google-client-secret>'[server.gateway.oidc]
issuer = "https://accounts.google.com"
client_id = "<google-client-id>"
client_secret_env = "SHUNT_GATEWAY_OIDC_SECRET" # default
allowed_domains = ["example.com"]
# allowed_emails = ["contractor@outside.example"]Google はデフォルトの openid email profile スコープを使います。shunt は Google の UserInfo が email_verified = true を返すことを要求し、そのうえで大文字小文字を区別しない完全なメールアドレスまたはドメインが許可リストに一致する場合にのみ、そのユーザーを受け入れます。
GitHub、SAML、その他 shunt が期待する標準の OIDC サーフェスを公開しないプロバイダーの場合は、Dex のような OIDC アイデンティティプロバイダーを前段に置き、その Dex の issuer をここに設定してください。プロバイダー固有の OAuth2 を直接統合することはスコープ外です。
issuer とすべてのエンドポイントは HTTPS を使う必要があります。平文の HTTP は localhost または 127.0.0.1 でのみ許可されます。承認ページの Content Security Policy が名前で指定できるループバックホストはその 2 つだけなので、[::1] やその他の 127.0.0.0/8 アドレス上の IdP は、後でブラウザーにブロックされる代わりに起動時に拒否されます。
空でない allowed_domains または allowed_emails のエントリが少なくとも 1 つ必要です。それがないと
shunt は起動を拒否します。[server.gateway.oidc] を設定すると users_env
は任意になります。SHUNT_GATEWAY_USERS を設定したままにすると SSO
とパスワードの両方のサインインが表示され、未設定にするとプロバイダーのボタンだけが表示されます。
ループバック以外のすべてのデプロイでは HTTPS を使ってください。デフォルトでは /device は X-Forwarded-For と X-Real-IP を無視し、ソケットのピアに対してレート制限をかけます。shunt へ信頼できるリバースプロキシ経由でのみ到達できる場合は、trust_forwarded_for = true を設定し、そのプロキシがクライアント由来の forwarding ヘッダーを削除してから自身の信頼できるクライアントアドレスを設定するよう構成してください。直接公開されたゲートウェイでは、このオプションを決して有効にしないでください。
2. 管理された Claude Code のログイン設定を配布する
各開発者マシンに次の管理設定を設定します。
{
"forceLoginMethod": "gateway",
"forceLoginGatewayUrl": "https://gateway.example.com"
}管理設定の場所はプラットフォームによって異なります。
- macOS:
/Library/Application Support/ClaudeCode/managed-settings.json - Linux と Windows (WSL):
/etc/claude-code/managed-settings.json - Windows ネイティブ:
C:\Program Files\ClaudeCode\managed-settings.json
この URL は public_url と一致していなければなりません。Claude Code は OAuth エンドポイントのパスを shunt の discovery ドキュメントから読み取ります。発行されたベアラーは、/v1/models と、選択されたプロバイダーがサーバー側の認証情報を注入する推論リクエストをゲートします。パススループロバイダーは開いたままです。
3. サインインする
Claude Code を起動して /login を実行します。CLI がデバイスコードを表示し、ゲートウェイの /device ページを開きます。そのページで:
- 表示されたデバイスコードを確認します。
- SSO ボタン(Google なら Sign in with Google、その他のプロバイダーなら Sign in with SSO)を選び、プロバイダーのログインを完了します。静的ユーザーも設定されている場合は、メールアドレスとシークレットを入力して Approve device を選ぶ方法も引き続き利用できます。
- 成功ページが表示されたら Claude Code に戻ります。
コードが事前入力されていても、それによって自動承認されることはありません。パスワード承認の POST は same-origin で保護されます。ゲートウェイはブラウザーの Sec-Fetch-Site: same-origin Fetch Metadata シグナルを信頼します。ページの Referrer-Policy: no-referrer により、ブラウザーは自身のフォーム送信でも Origin: null を送るためです。したがって Sec-Fetch-* ヘッダーを取り除くリバースプロキシを経由すると、すべての承認が “This request came from another site and was blocked.” で拒否されます。外部コールバックは代わりに、使い捨てで 10 分間有効な OAuth state と PKCE によってクロスサイトのリダイレクトを束縛します。プロバイダーのエラーがページへそのまま出力されることはありません。
ターミナルからサインインする
ユーザーごとの識別を保ったままゲートウェイに到達する方法は forceLoginMethod: "gateway" だけではありません。shunt は同じ device flow のクライアント側も提供しているので、ユーザーはターミナルからサインインし、Claude Code をサインイン済みのゲートウェイセッションにしないまま使えます:
shunt gateway login https://gateway.example.com # 同じ /device の承認をブラウザで
shunt gateway claude # ゲートウェイに接続した Claude Code を起動shunt gateway login は発行されたセッションを ~/.shunt/gateway/session.json に所有者専用で保存します(SHUNT_GATEWAY_SESSION_FILE で上書き可能)。続いて shunt gateway claude は、ゲートウェイの base URL と shunt gateway token を指す apiKeyHelper を含むインラインの --settings ドキュメントを付けて Claude Code を起動し、そのプロセス 1 つだけに適用します — ~/.claude/settings.json は一切変更されません。shunt gateway logout はセッションを破棄します。フラグの詳細は CLI リファレンスにあります。
何が変わり、何が変わらないか:
クライアント内の /login(forceLoginMethod) |
shunt gateway login + shunt gateway claude |
|
|---|---|---|
| ブラウザでの承認 | 必要 | 必要 — 同じ /device ページ、同じ same-origin 保護 |
| ゲートウェイ側でのユーザー識別 | あり | あり、同じ device flow のセッション |
| クライアント側の機能トレードオフ | 適用される | 適用されない — 認証情報が apiKeyHelper で届くため、クライアントは通常のファーストパーティモードのまま |
| モデルエイリアス | ゲートウェイセッションは opus/sonnet を旧 id に固定 |
通常のセッションと同じ |
| 認証情報の種類によるゲート | 適用される | こちらでも適用される — apiKeyHelper を渡すこと自体がトリガーで、セッションのゲートとは独立です(プロンプトキャッシュ TTL の既定値、Remote Control、音声入力、アーティファクトの公開) |
GET /managed/settings によるユーザー別ポリシー |
配信される | 取得しに行く動作は観測されず — クライアントにポリシーを強制させたい場合は forceLoginMethod: "gateway" を使用 |
トークンがどの認証情報スロットに届くかがクライアントのプロバイダーモードを決めます。上の表は Claude Code 2.1.234 で実測したもので、クライアントのリリースによって変わり得ます。
管理設定とモデルポリシー
サインイン後、shunt は認証済みの GET /managed/settings
から、そのユーザーに解決されたポリシーを返します。順序付きの [[server.gateway.policies]] エントリを設定します。
[[server.gateway.policies]]
[server.gateway.policies.match]
emails = ["alice@example.com"]
[server.gateway.policies.cli]
availableModels = ["claude-opus-4-8"]
[server.gateway.policies.cli.env]
DISABLE_UPDATES = "1"
[[server.gateway.policies]]
match = {} # catch-all
[server.gateway.policies.cli.permissions]
deny = ["WebFetch"]すべての catch-all エントリが順にマージされます。その上に、メールアドレス固有の最初の一致がマージされます。オブジェクトは再帰的にマージされ、許可リスト
の配列は置き換えられ、キーに deny を含む配列は重複なしで和集合になります。ポリシーが設定されていれば常に
200 を返します。ユーザー固有の設定も catch-all
の設定も適用されない場合、レスポンスにはテレメトリーが有効なら注入されたテレメトリーの env だけが含まれ、そうでなければ
{} になります。policies を省略すると 404 を返すため、Claude Code は「管理ポリシーなし」を区別できます。レスポンスには、安定したユーザーごとの
uuid、設定の checksum、そしてその checksum を含む RFC 準拠のクォート付き ETag
が含まれます。If-None-Match は変更がなければ 304
を返し、weak、カンマ区切りリスト、ワイルドカード、レガシーなクォートなしの validator も受け付けます。
availableModels が文字列の配列に解決される場合、shunt はそのゲートウェイユーザーに対して
/v1/messages と /v1/messages/count_tokens でもそれを強制します。比較の前に、クライアントが要求したモデルから末尾の
Claude Code のコンテキストウィンドウヒント([1m] または [1M])を 1 つ取り除くため、allowed[1m] は
allowed エントリに一致します。拒否されたモデルは、アップストリームに問い合わせることなく
400 invalid_request_error を受け取ります。
テレメトリーの受信
いずれかの signal を opt-in したテレメトリーの宛先リストは、2 つのことを同時に行います。管理された設定を通じてテレメトリーの有効化フラグと 5 つの OTEL_*
環境変数値を配布し(各 signal の exporter は、その signal を opt-in した宛先があれば otlp、なければ none。OTEL_EXPORTER_OTLP_ENDPOINT は public_url)、すべての管理されたクライアントの exporter をゲートウェイに向けます。さらに、クライアントがその後 POST する受信ルート POST /v1/metrics・POST /v1/logs・POST /v1/traces の verbatim 中継を有効にします。これらのルートは [server.gateway] が有効なら常に登録され、宛先が opt-in するまでは受理して破棄します。ポリシーの env キーは、注入されたデフォルトを上書きします。
[server.gateway.telemetry]
[[server.gateway.telemetry.forward_to]]
url = "https://collector.example.com"
# metrics = true # デフォルト
# logs = false # デフォルト
# traces = false # デフォルト
# headers = { "x-api-key" = "..." }url は OTEL_EXPORTER_OTLP_ENDPOINT と同じ形の base OTLP エンドポイントです。shunt は末尾の / を取り除いて signal のパスを付加するため、上の宛先は https://collector.example.com/v1/metrics を受け取ります。クエリ文字列、フラグメント、埋め込みの user:password は起動時に拒否されます。
宛先は signal ごとに opt-in します。metrics はデフォルトで on、logs と traces はデフォルトで off です。Claude Code の log record と span にはコマンドライン、プロンプト、ファイルパスが含まれ得るため、ホスト外へ送るのは明示的な選択であるべきだからです。受け取るべき宛先に logs = true や traces = true を設定してください。
受信ルートは /managed/settings と同じゲートウェイのベアラーを要求し、静的な [server.auth] トークンでは認証できません。ペイロードは verbatim に中継されます — 受信したリクエストのバイト列そのままで、受信時の content-type と content-encoding を保持し、宛先に設定された headers をその上に適用します(設定されたキーは転送値を置き換え、ヘッダーを重複させません)。クライアントの Authorization ヘッダーがコレクターへ転送されることはなく、中継はリダイレクトに従いません。
レスポンスは常に即座の 200 です。中継はデタッチされたタスクとして実行されるため、遅い・到達不能なコレクターがクライアントに見える遅延になることはなく、どの宛先も opt-in していない signal は拒否ではなく受理後に破棄されます。32 MiB の受信上限を超えるボディは 413 を返します。同時に実行される中継は最大 64 で、それを超えたペイロードはキューされず、警告とともに破棄されます。
セッションの挙動
アクセストークンは HS256 の JWT で、デフォルトの寿命は 1 時間です。Claude Code はそれを暗黙にリフレッシュします。リフレッシュのたびに不透明な refresh token がローテーションされます。保持していた古いトークンを、30 日・64 tombstone の範囲内で再生すると、そのローテーションファミリーのアクティブなトークンが無効化され、Claude Code は再度サインインすることになります。
デバイスグラントと試行カウンターはメモリ上にあります。refresh token のセッションは設定のホットリロードをまたいで維持され、後述のとおりデフォルトで永続化されます。署名シークレット、ユーザーリスト、OIDC 設定の変更はホット適用されます。期限切れのグラントとアイドルなレート制限エントリは日和見的に削除され、デバイスグラントとレート制限のアイデンティティはそれぞれ 4,096 エントリが上限です。使用済みの refresh token の tombstone は 30 日間保持され、ファミリーごとに 64 件が上限です。また、30 日間リフレッシュされなかったアクティブなセッションは期限切れになります。ルート登録は起動時に固定されるため、[server.gateway] テーブル自体の追加・削除には再起動が必要です。
refresh セッションはデフォルトで shunt の再起動をまたいで維持されます。shunt はグラントまたはローテーションのたびに refresh token のストアを state_path(デフォルトは ~/.shunt/gateway-sessions.json。アトミックに、所有者のみのパーミッション(Unix では 0600)で書き込まれます)へ保存し、起動時に復元します。そのためユーザーはブラウザーフローをやり直すことなく、リフレッシュを続けられます。refresh token は SHA-256 ハッシュとして保存されます — このファイルに使用可能な認証情報が含まれることはなく、トークンのハッシュとサインイン済みのアイデンティティだけが入ります。ファイルが存在しない、または破損している場合は、メモリのみの挙動にフォールバックするだけです。ホームディレクトリを解決できない環境でも同様です。メモリのみのセッションにするには state_path = "" を設定します。この場合、再起動で refresh セッションはクリアされ、アクセス JWT の期限が切れた時点でユーザーは再度サインインします。デバイスグラントはいずれの場合もメモリのみです(ログイン途中の再起動で失われるのは、その試行だけです)。また、state ファイルを同時に稼働する複数の shunt プロセスで共有してはいけません。
なお、refresh グラントはセッションとともに保存されたアイデンティティからトークンを発行し、静的ユーザーリストや外部 IdP の許可リストを再チェックしません。そのため、どちらの承認ソースからユーザーを削除しても、既存のセッションは終了しません。ユーザーを直ちにデプロビジョニングするには、state ファイルも削除(または state_path = "" を設定)したうえで再起動してください。
[server.auth] と [server.gateway] の両方が設定されている場合、両者は組み合わさります。有効な静的クライアントトークンか、有効なゲートウェイのベアラーのいずれかでアクセスが許可されます。これにより、既存のクライアントを壊すことなく段階的に移行できます。
この先の予定
管理ポリシー、ETag キャッシュ、テレメトリー環境変数の配布、認証済みのインバウンド
OTLP テレメトリーの受信と中継、サーバー側でのモデル許可リストの強制については上記のとおりです。