Codex App Serverは、自作アプリからCodexを操作するためのインターフェースです。会話履歴やコマンドの実行状況を扱えるため、社内ツールにコードレビューのボタンを追加するなど、独自の画面からCodexを利用できます。
この記事では、Codex CLI・SDKとの違い、Pythonから接続する手順、Flaskで「レビュー開始」ボタンを作る方法を解説します。Codex CLIとPythonの基本操作を知っている開発者を対象に、最小クライアントからWebアプリまで順に実装します。
扱うのは、ローカル環境で動作するデモです。複数ユーザー向けの認証設計や本番運用は対象に含めません。
ChatGPTのChat・Work・Codexの使い分けは、次の関連記事で紹介しています。
動作検証はCodex CLI 0.154.0で、2026年9月12日に実施したものです。仕様の出典は2026年9月17日に再確認しました。CLIのヘルプでは実験的機能とされているため、再現する際は利用バージョンを確認してください。

Codex App Serverは、Codex VS Code拡張などのクライアントにも使われる仕組みです。自作アプリから指示を送り、Codexが返す作業状況や回答を画面に表示します。認証状態、会話履歴、承認のやり取りも扱えます(公式ドキュメント)。
モデルのAPIを直接利用して同様のアプリを作る場合は、ファイル操作や実行権限、履歴管理などの設計も必要です。App Serverを使うと、Codexの仕組みを利用しながら、用途に合わせた画面や操作を実装できます。
| 方法 | 主な利用者 | 主な用途 |
|---|---|---|
| codex | 人間 | ターミナルで対話しながら開発する |
| codex exec | スクリプト | CIやバッチから非対話で1回実行する |
| Codex SDK | アプリ・CI | プログラムからタスクを制御する |
| codex app-server | 自作アプリ・IDE拡張 | Codexのクライアントを実装する |
ジョブの自動化やCIでの利用には、公式もCodex SDKを案内しています。会話履歴や承認画面、作業の進捗まで独自UIで扱いたい場合に、App Serverが選択肢になります。
通信方式は –listen で選びます。既定の標準入出力(stdio)では、改行で区切ったJSONを送受信します。本記事のPythonクライアントもこの方式を使います。
UnixソケットやWebSocketにも対応しています。たとえば、次の2つのコマンドを別々のターミナルで実行すると、WebSocketで待ち受けるApp ServerへCodex CLIから接続できます。
codex app-server --listen ws://127.0.0.1:4500
codex --remote ws://127.0.0.1:4500
Codex CLIと自作アプリは、どちらもApp Serverに接続するクライアントになれます。ただし、公式ドキュメントはApp ServerコマンドとWebSocket接続を実験的かつ本番非サポートとしています。stdioを選んだだけで本番利用が保証されるわけではありません。
通信には、JSONでメソッド呼び出しと応答をやり取りするJSON-RPC 2.0に沿った形式を使います。ただし、各メッセージのjsonrpcフィールドは省略します。リクエストは method・params・id を持ち、応答は同じ id を返し、通知は id を持ちません。
会話全体、1回の指示に対する処理、その中で生じる入出力を、次の3つに分けて扱います。
| 単位 | 意味 | 対応するメソッド・通知の例 |
|---|---|---|
| Thread | ユーザーとCodexの会話1本。複数のTurnを含む | thread/start、thread/resume、thread/fork |
| Turn | ユーザーの指示を受けてから、その指示への処理が完了するまでの単位 | turn/start、turn/interrupt、turn/steer |
| Item | Turnの中に積まれる入出力の最小単位 | item/started、item/completed、item/agentMessage/delta |
Itemには、エージェントの発言以外の作業も含まれます。ユーザーメッセージ、推論、シェルコマンドの実行、ファイル編集、MCPツール呼び出し、Web検索、レビューモードの開始と終了などが、それぞれItemの一種として届きます。0.154.0のスキーマでは19種類のItemが定義されていました。

クライアント側では、コマンド実行やファイル編集などのイベントを受け取り、作業の進み具合を表示できます。最終回答を待つ間も、利用者が現在の処理を把握できる仕組みです。
メソッドやパラメーターの定義をまとめたスキーマは、次のコマンドで生成できます。出力内容は、インストールされているCodexのバージョンに対応します。
codex app-server generate-json-schema --out ./schema
codex app-server generate-ts --out ./ts
0.154.0で生成したスキーマでは、クライアントから呼べるリクエストが99種類、サーバーからの通知が81種類、サーバーからクライアントへのリクエストが10種類ありました。サーバー発のリクエストには、コマンド実行やファイル変更の承認などが含まれます。アプリ側で承認画面を表示し、利用者の判断を返す際に使います。
PythonからApp Serverを子プロセスとして起動し、標準入出力でJSONを送受信します。Codex用の追加SDKは不要です。最小クライアントはPythonの標準ライブラリだけで動き、後半のWebアプリではFlaskを使います。
実行環境は下記のようにしました。
| 準備するもの | 条件 |
|---|---|
| Codex CLI | 本記事では0.154.0を使用。codex –versionで確認 |
| Python | 3.10以上 |
| uv | Python環境と依存関係の管理に使用 |
| Codexの認証 | 同じ実行ユーザーでcodex loginが完了していること |
| 対象リポジトリ | 調査対象のローカルGitリポジトリ。レビュー例では未コミット変更も必要 |
通信は、次の順序で進みます。
接続直後に initialize リクエストと initialized 通知を送るのが必須で、この手順を飛ばすと以降のリクエストは「Not initialized」で拒否されます。
"""codex app-server を子プロセスとして起動し、JSON-RPCで対話する最小クライアント。"""
import json
import subprocess
import threading
import time
from queue import Queue
class CodexAppServer:
def __init__(self, cwd: str):
self.cwd = cwd
self.proc = subprocess.Popen(
["codex", "app-server"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
bufsize=1,
)
self.events: Queue = Queue()
self._results: dict = {}
self._next_id = 0
threading.Thread(target=self._read_loop, daemon=True).start()
def _read_loop(self) -> None:
for line in self.proc.stdout:
line = line.strip()
if not line:
continue
message = json.loads(line)
if "method" in message:
self.events.put(message) # サーバからの通知
else:
self._results[message["id"]] = message # リクエストへの応答
def _send(self, method: str, params: dict, request_id: int | None = None) -> None:
message = {"method": method, "params": params}
if request_id is not None:
message["id"] = request_id
self.proc.stdin.write(json.dumps(message) + "\n")
self.proc.stdin.flush()
def call(self, method: str, params: dict | None = None, timeout: float = 120.0) -> dict:
self._next_id += 1
request_id = self._next_id
self._send(method, params or {}, request_id)
deadline = time.time() + timeout
while time.time() < deadline:
if request_id in self._results:
message = self._results.pop(request_id)
if "error" in message:
raise RuntimeError(f"{method}: {message['error']['message']}")
return message["result"]
time.sleep(0.05)
raise TimeoutError(method)
def initialize(self) -> dict:
client_info = {"name": "techfun_demo", "title": "Tech Fun Demo", "version": "0.1.0"}
info = self.call("initialize", {"clientInfo": client_info})
self._send("initialized", {})
return info
def start_thread(self) -> dict:
params = {"cwd": self.cwd, "approvalPolicy": "never", "sandbox": "read-only"}
return self.call("thread/start", params)["thread"]
def close(self) -> None:
self.proc.terminate()
このクライアントは、メソッド呼び出しへの応答と、サーバーから届くイベントを振り分けます。通知にはmethodとparamsが含まれ、idはありません。サーバー発のリクエストにはidも含まれますが、この例は承認を求めない設定のため、その応答処理は実装していません。
hello_appserver.pyでは、リポジトリの構成を説明するよう指示します。受信したイベントから、実行コマンドや回答文を取り出して表示します。
"""App Serverへ最初の1ターンを投げ、返ってくるイベントをそのまま表示する。"""
import sys
from codex_client import CodexAppServer
PROMPT = "このリポジトリの構成を1行で説明してください。"
def main() -> None:
cwd = sys.argv[1] if len(sys.argv) > 1 else "."
codex = CodexAppServer(cwd)
info = codex.initialize()
print(f"userAgent : {info['userAgent']}")
print(f"codexHome : {info['codexHome']}")
thread = codex.start_thread()
print(f"threadId : {thread['id']}")
print(f"session : {thread['path']}\n")
codex.call("turn/start", {"threadId": thread["id"], "input": [{"type": "text", "text": PROMPT}]})
while True:
event = codex.events.get()
method, params = event["method"], event.get("params", {})
if method in ("item/started", "item/completed"):
item = params["item"]
print(f"[{method}] {item['type']}")
if method == "item/completed" and item["type"] == "agentMessage":
print(f" -> {item['text']}")
if method == "item/completed" and item["type"] == "commandExecution":
print(f" -> $ {item['command']} (exit={item['exitCode']})")
elif method == "item/agentMessage/delta":
print(params["delta"], end="", flush=True)
elif method == "error":
print(f"[error] {params['error']['message']}")
elif method == "turn/completed":
print(f"\n[turn/completed] status={params['turn']['status']}")
break
codex.close()
if __name__ == "__main__":
main()
次のコマンドの /path/to/your/repo を、調査対象リポジトリのパスに置き換えて実行します。検証時はuserAgentとcodexHomeに続き、スレッドIDと作業イベントが表示されました。
uv run python hello_appserver.py /path/to/your/repo

turn/start に渡す input はテキストだけではありません。image、localImage、audio、localAudio を同じ配列に混ぜられるため、スクリーンショットを添えて指示するクライアントも作れます。
自作アプリで開始した会話は、同じ保存先を使うCodex CLIから再開できます。検証では、thread/startの応答に含まれるpathに、次のセッションファイルが返りました。
/home/ubuntu/.codex/sessions/2026/09/12/rollout-2026-09-12T09-19-35-01a092fb-dfe8-7862-96ce-725df9e80dcd.jsonl
保存先とファイル形式は、Codex CLIの対話セッションと共通です。次のコマンドのIDを、hello_appserver.pyで表示されたthreadIdに置き換えると、ターミナルから会話を続けられます。
codex resume 01a092fb-dfe8-7862-96ce-725df9e80dcd

実際に再開して「直前の回答を一言で繰り返してください」と入力すると、自作アプリ側で得ていた回答がそのまま返りました。
この結果から、検証環境では自作アプリからCLIへ会話を引き継げることを確認できました。別の環境でも利用する場合は、実行ユーザーとCodexの保存先(CODEX_HOME)が一致しているかを確認してください。
次は、ブラウザの「レビュー開始」ボタンからCodexを呼び出します。PythonのWebフレームワークであるFlaskを使い、未コミット変更のレビュー結果を画面へ表示します。
Codexのレビュー機能は review/start で呼び出します。targetに指定できる対象は、次の4種類です。
| target | レビュー対象 |
|---|---|
| {"type":"uncommittedChanges"} | ステージ済み・未ステージ・未追跡のファイル |
| {"type":"baseBranch","branch":"main"} | 指定ブランチとの差分 |
| {"type":"commit","sha":"abc1234"} | 特定のコミット |
| {"type":"custom","instructions":"…"} | 自由記述の指示 |
delivery は inline を指定し、既存スレッド上のTurnとしてレビューを実行します。レビュー用スレッドを分岐させるdetachedは、0.154.0のスキーマでは非推奨です。検証環境では「paginated threads do not support detached review」というエラーが発生しました。
レビュー専用のスレッドを分けたい場合は、thread/startで新しく作成してからinlineで実行します。
app.pyは、ボタンが押されるとApp Serverを起動し、レビューを開始します。サーバーからブラウザへ継続的にデータを送るServer-Sent Events(SSE)を使い、実行コマンドとレビュー結果を中継します。
"""「レビュー開始」ボタンを押すと review/start を呼び、結果をSSEで流すWebアプリ。"""
import json
import os
from flask import Flask, Response, render_template
from codex_client import CodexAppServer
app = Flask(__name__)
PROJECT_DIR = os.environ.get("REVIEW_PROJECT_DIR", os.getcwd())
def sse(label: str, text: str) -> str:
return f"data: {json.dumps({'label': label, 'text': text}, ensure_ascii=False)}\n\n"
@app.get("/")
def index() -> str:
return render_template("index.html", project_dir=PROJECT_DIR)
@app.get("/review")
def review() -> Response:
def stream():
codex = CodexAppServer(PROJECT_DIR)
codex.initialize()
thread = codex.start_thread()
yield sse("thread", thread["id"])
codex.call(
"review/start",
{
"threadId": thread["id"],
"delivery": "inline",
"target": {"type": "uncommittedChanges"},
},
)
while True:
event = codex.events.get()
method, params = event["method"], event.get("params", {})
if method == "item/completed":
item = params["item"]
if item["type"] == "exitedReviewMode":
yield sse("review", item["review"])
elif item["type"] == "commandExecution":
yield sse("command", f"$ {item['command']}")
elif method == "error" and not params["willRetry"]:
yield sse("error", params["error"]["message"])
elif method == "turn/completed":
yield sse("done", params["turn"]["status"])
break
codex.close()
return Response(stream(), mimetype="text/event-stream")
if __name__ == "__main__":
app.run(port=5000, threaded=True)
レビュー結果は exitedReviewMode というItemの review に、全体の説明と指摘の箇条書きがまとまったプレーンテキストとして入ります。指摘を1件ずつ構造化して受け取る必要がなければ、この文字列をそのまま表示すれば十分です。
templates/index.htmlには、開始ボタンとログの表示欄を置きます。ブラウザのEventSourceでSSEに接続し、受け取った内容を順に追加します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Codex Review Button</title>
<style>
body { font-family: sans-serif; margin: 2rem; max-width: 48rem; }
button { font-size: 1rem; padding: 0.6rem 1.4rem; }
pre { background: #f6f8fa; padding: 1rem; white-space: pre-wrap; }
.label { color: #2d6be4; font-weight: bold; }
</style>
</head>
<body>
<h1>Codex レビューボタン</h1>
<p>対象ディレクトリ: <code>{{ project_dir }}</code> の未コミット変更</p>
<button id="start">レビュー開始</button>
<pre id="log"></pre>
<script>
const log = document.getElementById("log");
document.getElementById("start").addEventListener("click", () => {
log.textContent = "";
const source = new EventSource("/review");
source.onmessage = (event) => {
const { label, text } = JSON.parse(event.data);
log.insertAdjacentHTML("beforeend",
`<span class="label">[${label}]</span> ${text}\n`);
if (label === "done") source.close();
};
});
</script>
</body>
</html>
REVIEW_PROJECT_DIRに、未コミット変更があるGitリポジトリのパスを指定します。起動後、ブラウザで http://127.0.0.1:5000/ を開いてください。
uv sync --locked
REVIEW_PROJECT_DIR=/path/to/your/repo uv run python app.py

ボタンを押すと、threadIdが表示され、Codexが git status や git diff を確認していく過程が流れ、最後にレビュー本文が届きます。

このデモは、codex loginで設定済みの認証を利用します。アプリのコードにOpenAIのAPIキーを記述する必要はありません。Codex側の認証は必要なので、ログイン済みのユーザーと同じ環境から起動してください。
検証時には、公式ドキュメントの例と0.154.0の列挙値が一致しない箇所がありました。thread/startのsandboxに workspaceWrite を渡すと、次のエラーが発生しました。
Invalid request: unknown variant `workspaceWrite`, expected one of `read-only`, `workspace-write`, `danger-full-access`
指定先によって、同じ概念でも表記が異なります。
| 指定先 | 書き込みを許可する値 |
|---|---|
| thread/startのsandbox | "workspace-write" |
| turn/startのsandboxPolicy | {"type": "workspaceWrite"} |
値が受け付けられない場合は、利用バージョンで生成したスキーマを確認してください。また、一部の実験的なメソッドやフィールドには、initializeの capabilities.experimentalApi をtrueにする設定が必要です。今回のサンプルで使う範囲では不要でした。
サンプルでは sandbox をread-only、approvalPolicyをneverにしています。read-onlyはファイルの書き込みを制限する設定です。neverは承認を求めない設定であり、あらゆる操作を許可する意味ではありません。
承認を求める設定に変更する場合は、サーバーから届く承認リクエストに応答する処理が必要です。代表例は item/commandExecution/requestApproval と item/fileChange/requestApproval です。サンプルはこの応答処理を省いているため、そのまま設定だけ変更すると承認待ちで停止する可能性があります。
error通知には、自動で再試行するかを示すwillRetryが含まれます。今回のWebアプリは、再試行しないエラーだけを画面に表示する構成です。
検証では、exitedReviewModeのreviewと、その後のagentMessageのtextに同じレビュー本文が届きました。両方を表示すると重複するため、app.pyでは前者だけを表示しています。
CLIのヘルプではApp Serverを実験的機能として扱っています。メソッドや列挙値は変わり得るため、バージョンを更新した際はスキーマと動作の再確認が必要です。本記事のデモだけでは、本番での安定性や複数ユーザーでの運用は判断できません。
企業向けに連携機能を開発する場合は、clientInfo.nameの扱いも確認してください。この値はOpenAIのコンプライアンスログでクライアントを識別するために使われます。公式ドキュメントは、企業向けの新しい連携について、既知のクライアント一覧へ追加するためOpenAIへ連絡するよう案内しています(初期化の説明)。
今回確認したのは、未コミット変更をレビューするローカルWebアプリです。同じ仕組みの応用例として、次のような機能が考えられます。いずれも、本記事では実装・動作検証していません。
| 活用例 | 利用するメソッド・追加する処理 |
|---|---|
| リポジトリごとのレビュー画面 | 対象ディレクトリを選び、review/startを実行する |
| ブランチ差分のレビュー | review/startのtargetをbaseBranchに変更する |
| 過去の会話一覧と再開ボタン | thread/listで一覧を取得し、thread/resumeで再開する |
| 実行中の作業への追加指示 | turn/steerで現在のTurnへ指示を送る |
たとえば、アプリで定型レビューを開始し、詳しく確認したい指摘があればCLIで会話を続ける運用が考えられます。Codex側の履歴を利用することで、用途ごとに操作画面を用意できます。
Codex App Serverを使うと、会話履歴や実行イベントを扱う独自のCodexクライアントを作れます。本記事では、Pythonからの接続、CLIでの会話再開、Flaskのレビューボタンまでを紹介しました。
まずは読み取り専用の設定でhello_appserver.pyを実行し、イベントの受信を確認してください。その後、レビューボタンの実装へ進むと、Codexとの通信と画面表示を順に理解できます。設定や機能を追加する際は、利用バージョンのスキーマを確認し、承認への応答など必要な処理を補ってください。
参考リンク:
Tech Funでは、お客様のフェーズに合わせ、生成AI活用に向けた様々な支援をご提供しています。
生成AIに限らず、Web・業務システム開発やインフラ設計など、技術領域を問わずご相談を承っています。「何から始めれば良いか分からない」という段階でも構いませんので、ぜひお気軽にお問い合わせください。