Codexで使えるモデルを増やしたい、という需要に対してCodex Routerは変わった答えを出している。別のCLIを被せるのでも、設定ファイルを毎回書き換えるのでもなく、Codexが読むモデルカタログそのものに外部モデルを合流させ、純正のモデルピッカーにGPTと並べて表示させる。だが同時にこれは、複数プロバイダの資格情報を預かるローカルHTTPサービスを常駐させるということでもある。本記事は使い方の紹介ではなく、その信頼境界——誰がその窓口を叩けて、認証なしで何が見えるのか——をv0.4.0-beta.3のソースと実際の起動で測った記録である。

Codex Routerの全体像:31プロバイダ・80モデル・ローカル中継の構成
Codex Router v0.4.0-beta.3 のモデルレジストリを実際に読み込んで数えた構成。数値は本記事の実測値(duolahypercho/codex-router)。
30秒でわかるポイント
  • 何をするか:Kimi・DeepSeek・Grok・Anthropic・GitHub Copilotなどの外部モデルを、Codexの純正モデルピッカーに並べる。中継はローカルで動き、Responses APIを話す。
  • 規模の実測:レジストリを読み込むとプロバイダ31件(正準27+プロトコル変種4)、ピッカー掲載モデル80件。内訳はOpenAI互換29・OAuth 2。
  • 鍵は2つ、揃わないと起動しない:caller keyとinternal keyの両方が必須。片方欠けるとリッスン前に例外で落ちる(実測)。
  • 認証はヘッダではなくURLのパス/_codex-router/<32文字以上の鍵>/v1。Codexの設定にベースURLしか書けないための設計で、鍵違いは401(実測)。
  • 認証なしで見えるのは4フィールドだけGET /healthは認証不要だが、認証後の8フィールドからrouter/gateway/api/oauthが落ちる(実測)。
  • 注意点:待ち受けホストはCODEX_ROUTER_HOSTで上書きでき、値を検証するコードは無い。「0.0.0.0にするな」は文書上の指示であってコードの強制ではない。

AIコーディングツールの全体像と、その中でルーター系ツールがどの位置にいるかはVibe Codingとは?AIコーディングの始め方・ツール比較・実践ワークフロー2026にまとめてある。本記事はそこから一段掘って、ローカル中継の内部を見る。

Codex Routerとは——Codexのモデルピッカーに外部モデルを並べる仕組み

Codex Routerは、Codex(CLIおよびアプリ)から外部モデルを使うためのローカルルーターだ。MITライセンス、作者はduolahypercho、2026-07-19に公開され、本記事執筆時点で★2,114・fork 147。公開から4週間足らずでこの数字が付いている。リポジトリは明確に「独立したコミュニティプロジェクトであり、OpenAI・GitHub・Anthropic・Moonshot AI・DeepSeek・OpenRouter・opencodeとは無関係」と宣言している。

このツールに辿り着く動機はだいたい具体的だ。DeepSeekのAPIキーを持っているのでCodexから使いたい(codex deepseek)、KimiのCLIで既にOAuth済みなのでその枠をCodexでも使いたい(codex kimi)、要するにcodex 外部モデルの追加をやりたい——という形をしている。汎用のモデルルーターは「どのエージェントからどのモデルへでも流せる」ことを売りにするが、そこまでは要らず、いま使っているCodexの中だけで選択肢が増えればいいという需要が確実にある。Codex Routerはそこに正面から答えている。

同種のツールと決定的に違うのは出口である。多くのルーターは「エージェントの向き先を自分に変えさせる」ところで止まるが、Codex Routerは外部モデルの情報をCodexのネイティブモデルカタログにマージする。結果として、routedモデルは設定画面の奥ではなく、普段GPTを選んでいるのと同じピッカーに並ぶ。

対応プロバイダの内訳と設定ディレクトリ
レジストリ実測:プロバイダ31件のうちOpenAI互換29・OAuth 2。config/配下のプロバイダ別ディレクトリは25個。

実際に何件のモデルが並ぶのか

READMEは対応プロバイダを列挙するが、件数は書き方によって変わる。そこでレジストリを直接読み込んで数えた。

プロバイダ総数31件——うちvariantOfを持たない正準プロバイダが27件、同じ資格情報を共有するプロトコル変種が4件
ピッカー掲載モデル80件LISTED_MODELS
kind別内訳openai-compatible 29件、oauth 2件
config/配下のプロバイダ別ディレクトリは25個(anthropic / cerebras / chutes / clinepass / commandcode / deepseek / fireworks / gemini / github-copilot / grok / groq / huggingface / kimi / local / meta / minimax / mistral / nvidia / ollama / opencode / openrouter / qwen / siliconflow / together / zai)

プロトコル変種が独立してカウントされる点は実装上の重要な性質だ。変種は親の資格情報を共有するため、親と切り離して単独で選択できない設計になっている。選択ファイルには正準の親IDだけが保存され、読み出し時に変種へ展開される。変種が後から追加されても、以前に書かれた選択がそのまま新しい変種を有効にする——という互換性のための作りである。

インストールと起動——鍵が2つ揃わないと動かない

配布はHomebrewのタップとリポジトリ同梱のインストーラの2系統。公式の手順は次の通りで、setup --guidedがプロバイダ選択・資格情報の投入・バックグラウンドサービス導入・Codex統合までを一度に行う。

brew tap duolahypercho/codex-router https://github.com/duolahypercho/codex-router
brew install codex-router
codex-router setup --guided

ただし本記事の目的は信頼境界の観察なので、インストーラは使わずに中継プロセスだけをサンドボックスで起動した。CODEX_HOMEと状態ディレクトリを一時パスへ逃がせば、既存の~/.codex/config.tomlにもlaunchdにも一切触れずに済む。

起動ゲートを1つずつ剥がす

素の状態から起動すると、必要なものが順番に例外として現れる。これが実測した起動ゲートの階段である。

第1のゲート:Node.jsのバージョン。 package.jsonenginesnode >= 22.19.0。これは推奨ではなく実質的な必須条件で、Node 22.13.1では以下のようにリッスン以前に落ちる

SyntaxError: The requested module 'node:zlib' does not provide an export named 'zstdCompress'

router.mjsnode:zlibからzstdCompressをインポートしているためだ。zstd対応が入る前のNodeでは、モジュール解決の時点で失敗する。25.2.1に替えると通った。

第2のゲート:caller key。 未設定だとassertCallerSecretが「The local router caller key is missing or invalid」で停止する。

第3のゲート:internal key。 caller keyだけ与えても、次はこれで止まる。

Error: CODEX_ROUTER_INTERNAL_KEY is required.

両方を与えて初めてリッスンに到達する。鍵が欠けたまま「とりあえず動いてしまう」経路は無い——fail-closedである点は、資格情報を預かるサービスとして妥当な作りだ。

# サンドボックス起動(~/.codex には触れない)
export CODEX_HOME=/tmp/sandbox/codexhome
export CODEX_ROUTER_STATE_DIR=/tmp/sandbox/state
export CODEX_ROUTER_CALLER_KEY=$(node -e "console.log(require('crypto').randomBytes(24).toString('base64url'))")
export CODEX_ROUTER_INTERNAL_KEY=$(node -e "console.log(require('crypto').randomBytes(24).toString('base64url'))")
export CODEX_ROUTER_PORT=18899
node src/router.mjs
# => [codex-router] listening

起動後、待ち受けを確認すると127.0.0.1のみだった。

lsof -nP -iTCP:18899 -sTCP:LISTEN
# node  14394 user  14u  IPv4  TCP 127.0.0.1:18899 (LISTEN)

誰がこのローカル窓口を叩けるのか——パス埋め込みのcaller key

ここが本ツールで最も設計判断が出ている部分だ。ローカルに立つHTTPサービスは、同じマシンのどのプロセスからも、そしてブラウザで開いたページからも叩ける。Codex Routerはそれを32文字以上のランダムなcaller keyで塞いでいる。

caller keyがヘッダではなくURLパスに埋まる理由
認証情報をヘッダに置けない制約と、その帰結。パスに鍵が乗るためURL自体が秘密になる。

なぜヘッダではなくURLのパスなのか

普通ならAuthorizationヘッダを使う。しかしCodex側の設定に書けるのはベースURLだけで、ネイティブのクライアント経路に独自の認証ヘッダを差し込む余地がない。そこでこのツールは鍵をパスに埋めた。

http://127.0.0.1:<port>/_codex-router/<32文字以上の鍵>/v1

caller-auth.mjsの実装はごく短いが、要点が詰まっている。

・鍵の条件は32文字以上かつ[A-Za-z0-9_-]のみ(validCallerSecret
・比較はnode:cryptotimingSafeEqual——長さを先に比べ、一致時のみ定数時間比較に入る
・パス先頭が/_codex-router/でなければ即座に不一致
redactCallerUrlという専用の伏字関数があり、設定書き出し・移行・スモークテスト・互換テストの各所で使われている

最後の点がこの設計の代償を物語っている。鍵がURLに乗る以上、URLはログやエラー出力に漏れやすい。実際config-manager.mjsopenai_base_urlを記録する際に伏字を通し、SECURITY.mdも「Codexが自身のエラー出力にリクエストURLを含めることがある。ループバック限定であってもURL全体を機微情報として扱い、スクショやログを共有する前に伏せること」と明記している。

鍵違いは何を返すか

実測した。43文字のデタラメな鍵を与えると401が返る。

# 誤った鍵
curl -s -o /dev/null -w "%{http_code}\n" \
  "http://127.0.0.1:18899/_codex-router/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/v1/health"
# => 401

本文は次の通りで、鍵の長さや形式の当たり外れを区別する情報は返さない。

{"error":{"type":"authentication_error","message":"This local router endpoint requires its configured caller capability."}}

リポジトリ同梱のテストにもこの挙動は含まれている。node --test test/caller-auth.test.mjsは「caller capability helpers accept only the secret-bearing path and redact it」「secret setup creates stable, separate, current-user-only keys」の2件が通る。

認証なしで見える情報・見えない情報——/healthの4対8

モデル経路が全部401で守られているなら、認証なしで叩ける口はあるのか。1つだけある。 GET /healthは、caller keyの判定より手前で処理される。

認証なしのhealthと認証後のhealthのフィールド差
実測したフィールド差。認証なしは4キー、認証後は8キー。差分はrouter・gateway・api・oauthの4つ。

処理順はソースの通りで、/healthを返してからauthenticatedRoute()の判定に進む。つまり認証なしのGETが1経路だけ存在する。ただし返す内容は絞られている。同じhealthPayload()から、認証なしの応答では一部のフィールドだけを手で選んで返している。

実測結果を並べる。

経路 認証 HTTPステータス 返却キー数 キー
GET /health 不要 503 4 ok / service / version / activity
GET /_codex-router/<鍵>/v1/health 必要 503 8 ok / service / version / activity / router / gateway / api / oauth
GET /_codex-router/<誤鍵>/v1/health 失敗 401 error のみ

※ステータス503は、サンドボックスでプロバイダを構成していないためok:falseになった結果であり、認証の成否とは独立している。

認証なしで得られるのはこれだけだ。

{"ok":false,"service":"codex-router","version":"0.4.0-beta.3",
 "activity":{"state":"idle","activeCount":0,"active":[]}}

落ちている4フィールドの意味が重要で、router(中継自身の準備状態)・gatewayapioauthはそれぞれ下位サービスの到達性を示す。healthPayload()の実装を読むと、oauthapi有効化されたプロバイダの種類に応じて探索するかどうかが変わる。つまりこの4フィールドは間接的に「どの系統のプロバイダを構成しているか」を示唆する。それを認証なしの応答から外しているのは筋が通っている。なお資格情報そのものは認証後の応答にも含まれない

認証なしの側に残っているactivity

一方でactivityは認証なしの側に残っている。ここは注意して読む価値がある。activityPayload()の戻り値はstateactiveCountactiveに加えて、直近にルーティングしたprovidermodelsessionNameが設定されていれば付与されるという条件付きの形になっている。これらはsetRoute()lastUsedProviderlastUsedModellastUsedSessionNameへ書き込み、リクエスト終了後も保持される。

筆者のサンドボックスではプロバイダを構成しなかったため、実測できたのはstate:"idle"activeCount:0active:[]のアイドル状態までである。「実際に外部モデルの名前が認証なしで見えた」ところまでは確認していない——確認したのは、①activityが認証なしの応答に含まれること、②activityの構造が直近のprovider/model/sessionNameを含みうること、の2点だ。この2つを踏まえると、稼働中のインスタンスではどのプロバイダのどのモデルを使っているかという利用メタデータが、鍵を持たない同一ホスト上のプロセスから読める可能性がある。資格情報の漏洩ではないが、ローカルに常駐させる以上は把握しておきたい性質である。

資格情報の置き場所と送り先——600固定とホスト固定

信頼境界のもう半分は「預けた鍵がどこに置かれ、どこへ飛ぶか」だ。

資格情報の保存場所と送信先の制約
状態ディレクトリのファイルは600固定。送信先はプロバイダ自身のホストに固定される。

置き場所。 状態は既定で$CODEX_HOME/codex-router配下に置かれ、SECURITY.mdの表ではinternal-secretcaller-secret・各プロバイダの*-api-key.secretlitellm.yaml・カタログ類まですべてモード600が指定されている。実装側ではfile-security.mjsprotectPrivateFile()chmodSync(target, 0o600)を実行し、Windowsではicaclsで継承ACLを外して現在のユーザーSIDにのみフルコントロールを与える。検査側のprivateFileIsProtected()はPOSIXで(mode & 0o777) === 0o600という厳密一致で判定する——640でも644でも不合格になる。

インストーラの方針も明示されている。APIキーの環境変数をlaunchd・systemd・タスクスケジューラの定義へコピーしない。 環境変数だけの資格情報はフォアグラウンド実行では動くが、バックグラウンドサービスには保護されたファイルが要る、という割り切りだ。サービス定義ファイルは一般に読み取り権限が緩いので、この判断は妥当である。

送り先。 「各フェッチャは資格情報をプロバイダ自身のホスト以外へ送らない」という主張がCHANGELOGにあり、実装で確認できる。たとえばGitHub Copilotの経路では、トークンを送る前に接続先を検証している。

if ((url.protocol !== "https:" || url.hostname !== "api.github.com") && !loopback) {

httpsかつapi.github.comでなければ弾く(テスト用のループバックのみ例外)。SECURITY.mdはさらに、アカウント情報が選択した推論エンドポイントもGitHub所有のCopilotホストへ許可リスト方式で絞ってからトークンを送る、と説明している。

外部リクエストから外されるものも明記されている。ChatGPTアカウントID・Codexインストール識別子・attestationヘッダ・呼び出し元のauthorizationヘッダは外部プロバイダへ渡らない。ループバック区間は内部鍵で通し、最終フォワーダがそれをちょうど1つのプロバイダ資格情報に差し替える構造だ。

flowchart TD A["Codex(CLI / アプリ)"] -->|"ベースURLに鍵が埋まっている"| B["router :127.0.0.1"] B --> C{"パスが
/_codex-router/<鍵>/ か"} C -->|"不一致"| D["401
authentication_error"] C -->|"GET /health のみ
判定より手前"| E["4フィールドだけ返す
ok / service / version / activity"] C -->|"一致"| F["内部鍵でゲートウェイへ"] F --> G["最終フォワーダが
内部鍵を1つの
プロバイダ資格情報に差し替え"] G --> H["プロバイダ自身のホストのみ
(例 api.github.com)"]

明示されている限界

SECURITY.mdは自分の守備範囲を過大に主張していない。これらの制御が防ぐのはブラウザからの流れ弾と、利用者の保護ファイルへアクセスできないプロセスである。一方で「同じOSユーザーとして既に動いている悪意あるコードに対する境界にはならない」と明言している。同じユーザーで動くコードは、そもそもCodexの設定やプロセス状態を読めるからだ。

そしてここが実務上いちばん効く注意点になる。待ち受けホストは環境変数で上書きできる。

const LISTEN_HOST =
  process.env.CODEX_ROUTER_HOST || process.env.KIMI_ROUTER_HOST || "127.0.0.1";

CODEX_ROUTER_HOSTはソース全体でこの1箇所からしか参照されておらず、値を検証するコードは見当たらない。SECURITY.mdの「リスナを0.0.0.0に変えるな、ポートをトンネルするな、共有ネットワークに晒すな」は文書上の指示であって、コードによる強制ではない。既定は安全側だが、安全側に固定されているわけではない、と理解しておくのが正確だ。同様にAPIベースURLの上書きも「信頼されたユーザーの設定」という扱いで、悪意ある上書きは対応するプロバイダ資格情報を別のサーバーへ送りうる、と自ら注意書きしている。

Codex Routerと類似ツールの比較——どれが自分の問題を解くか

「Codexまわりの拡張」は既にいくつもあるが、解いている問題が違う。混同しやすいので実際の対象で整理する。

ツール 対象 何をするか モデルピッカー統合 資格情報の保管
Codex Router Codexのみ 外部モデルをCodexの純正カタログへマージし、Responses APIで中継 あり(純正ピッカーに並ぶ) 状態ディレクトリに600で保存
Claude Code Router 9種のエージェント 複数エージェントの向き先を1つのローカル窓口に束ねる汎用ゲートウェイ なし(ルーター側で切替) ルーター側の設定で管理
oh-my-codex(OMX) Codex CLI HUD・チーム実行・hooksなどワークフロー層を足す 対象外 対象外
codex++ Codex Desktop 再ビルドせずデスクトップアプリを拡張するTweak 対象外 対象外

選び分けの軸は2つ。 ①複数のエージェントを横断して束ねたいのか、Codex 1つに深く統合したいのか。②モデル選択をCodexのピッカー内で完結させたいのか、ルーター側の設定で切り替えてよいのか。Codex Routerが刺さるのは「Codexしか使っていないが、Codexの中でKimiやDeepSeekも選びたい」という形だ。逆に複数エージェントを行き来するなら向きが違う。

Codex CLIそのものの導入・認証・サンドボックス設定はOpenAI Codex CLIの使い方2026|導入・認証からClaude Code比較まで完全ガイドにまとめてある。

使い分けのヒント
「外部モデルを足したい」と「エージェントを束ねたい」は別の問題である。前者だけが目的なら対象を絞ったツールのほうが設定点が少なく、壊れたときの切り分けも早い。Codex Routerがロールバックとマイグレーションのスナップショットを備えているのは、Codexの設定を書き換える側に回るツールだからだ。

導入前に自分の環境で確認しておくこと

Codex Routerに限らず、資格情報を預かるローカル常駐サービスを入れるときは、入れた後に自分で測れる状態にしておきたい。以下は本記事の検証で実際に使ったコマンドである。

① 待ち受けが本当にループバックだけか。 外部に晒れていないかはこれで分かる。127.0.0.1以外(特に*:0.0.0.0)が出たら設定を疑う。

lsof -nP -iTCP -sTCP:LISTEN | grep -i node
# 期待: TCP 127.0.0.1:<port> (LISTEN)

② 資格情報ファイルのパーミッションが600か。 検査側は600の厳密一致なので、実ファイルもそれに揃っているべきである。

ls -l "${CODEX_HOME:-$HOME/.codex}/codex-router"/*.secret
# 期待: -rw------- (600以外が出たら要確認)

③ 認証なしで何が見えるか。 自分のインスタンスで実際に叩いてみるのが確実だ。

curl -s http://127.0.0.1:<port>/health
# 4フィールド(ok/service/version/activity)以外が出るなら想定と違う

④ Codexの設定が何を書き換えられたか。 初回変更前のバックアップが作られるので、差分を見れば管理範囲が分かる。

diff -u ~/.codex/config.toml.pre-codex-router ~/.codex/config.toml

設定マネージャはopenai_base_urlmodel_catalog_jsonのブロックだけを印付きで書き、modelmodel_provider・推論設定・プロファイル・ChatGPT認証は保存する、と説明されている。印の無いユーザー所有のベースURLやカタログは置き換えを拒否する作りだ。実際に何が変わったかは、この差分で自分の目で確かめられる。

導入前に理解しておくこと
・本ツールはOpenAIの公式製品ではない。独立したコミュニティプロジェクトである
・バージョンは0.4.0-beta.3——ベータであり、仕様は動く前提で見るべき段階にある
・便宜的なブートストラップ手順はリポジトリの既定ブランチを追う。完全にレビュー可能な導入を求めるなら、タグ付きアーカイブを取得してSHA256SUMSとビルドプロヴェナンスを検証してから、ローカルのインストーラを実行する手順が案内されている
・同一OSユーザーで動く悪意あるコードに対しては境界にならない、と作者自身が明記している

まとめ——「便利さ」の裏側を見てから入れる

Codex Routerは、Codexのモデルピッカーに外部モデルを並べるという分かりやすい価値を提供する。その裏側でやっていることは、複数プロバイダの資格情報を預かるローカルHTTPサービスの常駐である。本記事で実測して確認できたのは次の点だ。

・鍵が2つ揃わないと起動しない(fail-closed)。Nodeのバージョン要件も実質必須で、22.13.1では起動前に落ちる
・モデル経路は全て32文字以上のcaller keyで保護され、鍵違いは401。鍵はURLのパスに埋まるため、URL自体が機微情報になる
・認証なしで叩けるのはGET /healthの1経路のみで、返るのは4フィールド。認証後の8フィールドから下位サービスの状態が落ちている
・資格情報は600固定で保存され、送信先はプロバイダ自身のホストに絞られる
・ただし待ち受けホストの上書きはコードで検証されていない。既定が安全側であることと、安全側に固定されていることは違う

★2,114という数字は、Codexで使えるモデルを増やしたいという需要の大きさをそのまま映している。入れる価値のあるツールだと思う。ただし常駐させるのは「自分の鍵束を預けた窓口」なので、上記4つの確認コマンドくらいは、入れた日に一度自分で走らせておきたい。

参照ソース

duolahypercho/codex-router — GitHubリポジトリ(README.md / SECURITY.md / CHANGELOG.md / package.json、および src/caller-auth.mjssrc/router.mjssrc/file-security.mjssrc/github-copilot-session.mjs。v0.4.0-beta.3 時点、2026-08-14 確認)
codex-router — SECURITY.md(セキュリティモデル)(資格情報の分離・ローカル秘匿ストレージの一覧とモード・ネットワーク境界の記述)
OpenAI Codex — 公式リポジトリ(Codex本体のモデルプロバイダ設定と設定ファイルの扱い)
・本記事の実測環境:macOS(Darwin 23.5.0)、Node.js 22.13.1 および 25.2.1、対象コミット時点の main、サンドボックスは CODEX_HOMECODEX_ROUTER_STATE_DIR を一時ディレクトリへ退避して起動