codex execをスクリプトから呼んだものの、最終メッセージに「完了」と書かれているだけで成功扱いしてよいのか。途中のコマンドが失敗したとき、どこを見ればよいのか。自動処理へ進むほど、この判断に迷いやすくなります。
結論から言うと、最終メッセージだけでは足りません。少なくとも次の3点を別々に確認します。
- Codex CLIプロセスの終了コード
- JSONLに記録されたイベントと、途中のコマンド終了状態
- 期待した成果物の内容と、対象外ファイルに変更がないこと
この記事では、記事専用の架空Markdownを1ファイルだけ整形した実測をもとに、成功、空入力、不正なJSONL、CLI起動失敗、エージェント内コマンド失敗の切り分け方を紹介します。Web公開や外部サービス更新までは扱いません。
繰り返し使う対象範囲や確認コマンドは、AGENTS.mdでCodexの作業ルールを整理する方法でまとめています。今回だけの依頼と共通ルールを分けてから、非対話実行を試すと確認項目をそろえやすくなります。
今回の検証条件
実在する業務資料やリポジトリは使わず、隔離した一時ディレクトリに架空の会議メモだけを置きました。
| 項目 | 条件 |
|---|---|
| 検証日時 | 2026年10月9日 06:02 UTC |
| OS | Linux 4.18.0、x86_64 |
| Codex CLI | codex-cli 0.162.0 |
| モデル | gpt-5.6-sol |
| 対象 | meeting-note.md 1ファイル |
| サンドボックス | workspace-write |
| セッション | --ephemeral |
| 出力 | --jsonによるJSONL |
APIキーや保存済み認証の内容は、fixture、プロンプト、出力、検証記録へ含めていません。workspace-writeは書込みを許可する設定なので、作業ディレクトリには記事用fixture以外を置かない構成にしています。
入力は次の短いメモです。
|
1 2 3 4 5 6 7 8 9 |
# 架空プロジェクト定例メモ 日時 2030-04-01 10:00 参加者 Aさん Bさん 決定 デモ画面の文言を短くする 担当 Aさん 期限 2030-04-03 担当 Bさん 期限 2030-04-05 |
依頼内容は、見出しと箇条書きを加え、意味、名前、日時、期限を変えないこと。変更対象はこの1ファイルだけとしました。
codex execの出力を3つに分ける
OpenAI公式資料によると、通常のcodex execは進捗を標準エラーへ、最後のエージェントメッセージを標準出力へ出します。--jsonを付けると、標準出力は1行に1つのJSONオブジェクトを置くJSON Lines(JSONL)になります。
今回の最小呼び出しは、次の形です。WORK_DIRは架空Markdownだけを置いた一時ディレクトリ、ログはその外側へ保存します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
codex exec \ --skip-git-repo-check \ --ephemeral \ --ignore-user-config \ --ignore-rules \ --model gpt-5.6-sol \ --sandbox workspace-write \ --json \ -c 'approval_policy="never"' \ -c 'model_reasoning_effort="low"' \ -C "$WORK_DIR" \ - < prompt.txt \ > run.stdout.jsonl \ 2> run.stderr.txt process_exit=$? printf '%s\n' "$process_exit" > run.exit-code.txt |
この検証では、成功時に次の順でイベントが記録されました。
|
1 2 3 4 5 6 7 8 9 10 |
thread.started turn.started item.completed agent_message item.started file_change item.completed file_change item.started file_change item.completed file_change item.completed agent_message turn.completed |
プロセス終了コードは0で、変更後のMarkdownは期待値とバイト単位で一致しました。作業ディレクトリには対象ファイル以外を置いていないため、対象外のファイル変更もありませんでした。
一方、標準エラーには、途中で失敗したファイル変更操作の記録が1件残りました。その後に別の操作で期待した変更が完了しています。この例からも、標準エラーが空かどうか、最終文が成功を名乗っているか、という1点だけで判定しないほうが安全です。

JSONLは1行ずつ解析する
JSONL全体を1つのJSONとして読み込むのではなく、空でない各行を順にjson.loads()へ渡します。次の判定器は、プロセス非ゼロ、JSONLの構文エラー、ターン失敗、途中コマンドの非ゼロ、成果物不一致を別の終了コードで返します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
#!/usr/bin/env python3 """Codexの終了状態、JSONL、期待成果物を別々に検査する。""" import argparse import json from pathlib import Path import sys parser = argparse.ArgumentParser() parser.add_argument("--process-exit", type=int, required=True) parser.add_argument("--jsonl", type=Path, required=True) parser.add_argument("--actual", type=Path, required=True) parser.add_argument("--expected", type=Path, required=True) args = parser.parse_args() if args.process_exit != 0: print(f"PROCESS_FAILED exit={args.process_exit}", file=sys.stderr) raise SystemExit(7) events = [] for line_no, raw in enumerate(args.jsonl.read_text(encoding="utf-8").splitlines(), 1): if not raw.strip(): continue try: events.append(json.loads(raw)) except json.JSONDecodeError as exc: print(f"JSONL_PARSE_ERROR line={line_no}: {exc.msg}", file=sys.stderr) raise SystemExit(2) types = [event.get("type") for event in events] if "turn.failed" in types or "error" in types or "turn.completed" not in types: print("TURN_NOT_COMPLETED", file=sys.stderr) raise SystemExit(5) failed_commands = [ event["item"] for event in events if event.get("type") == "item.completed" and isinstance(event.get("item"), dict) and event["item"].get("type") == "command_execution" and (event["item"].get("status") == "failed" or event["item"].get("exit_code") not in (None, 0)) ] if failed_commands: print(f"COMMAND_FAILED count={len(failed_commands)}", file=sys.stderr) raise SystemExit(6) if args.actual.read_bytes() != args.expected.read_bytes(): print("OUTPUT_MISMATCH", file=sys.stderr) raise SystemExit(8) print("RESULT_OK") |
この判定器を保存済みの実測データとfixtureへ適用した結果は次のとおりです。
| ケース | Codexプロセス | JSONLまたは成果物 | 判定器の終了コード |
|---|---|---|---|
| 成功 | 0 | turn.completed、期待値と一致 |
0 |
| エージェント内コマンド失敗 | 0 | command_executionが終了7 |
6 |
| CLI引数エラー | 2 | JSONLなし、成果物無変更 | 7 |
| 壊れたJSONL fixture | 0として検査 | 3行目で構文エラー | 2 |
| 成果物不一致 | 0 | JSONL完了、期待値と不一致 | 8 |
ここで重要なのは、「turn.completedがある」と「成果物が正しい」を別条件にしたことです。未知のイベントが現れた場合も、黙って既知イベントへ読み替えず、記録したうえで判定器の更新要否を確認します。
空入力はCodexを起動する前にも止める
対象ファイルが空なら、モデルを呼ぶ前に止められます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#!/usr/bin/env python3 """空入力をCodex起動前に拒否する最小例。""" from pathlib import Path import sys path = Path(sys.argv[1]) if not path.exists() or not path.read_text(encoding="utf-8").strip(): print(f"EMPTY_INPUT: {path.name}", file=sys.stderr) raise SystemExit(10) print("INPUT_OK") |
空のempty.mdに対して実行すると、標準エラーはEMPTY_INPUT: empty.md、終了コードは10となり、Codexは起動しませんでした。
別に、codex execへ標準入力からプロンプトを渡さないケースも確認しました。この場合は標準エラーにNo prompt provided via stdin.と出て、Codex CLIは終了コード1、JSONLなし、対象ファイル無変更でした。入力ファイルが空なのか、CLIへ依頼文が渡っていないのかを分けて記録できます。
CLI失敗と、エージェント内コマンド失敗は別物
安全なCLI失敗例として、存在しないオプション--article-invalid-optionを指定しました。CLIは使用方法を標準エラーへ出し、終了コード2で終了しました。モデル処理は始まらず、JSONLは空、対象ファイルも無変更です。
一方、エージェントへ無害なsh -c 'exit 7'を1回だけ実行させたケースでは、JSONLのcommand_executionに終了コード7とstatus: failedが記録されましたが、Codex CLIプロセス自体は終了コード0で、ターンもturn.completedでした。
つまり、CLIの終了コード0だけでは、途中のコマンド成功までは保証できません。JSONL内のcommand_executionも調べ、1件でも非ゼロなら成果物を採用しないルールにします。
--output-schemaと--jsonを混同しない
--jsonは実行中のイベント列をJSONLで出すための指定です。--output-schemaは、最終応答を指定したJSON Schemaへ合わせるために使います。同じ「JSON」という言葉が出ますが、検査対象が異なります。
今回の実測では--output-schemaを使っていません。導入する場合は、少なくとも次を追加で確認します。
- schema全文を検証記録へ残す
- 最終応答をJSON Schema検証器へ通す
- schema準拠と内容の正しさを別々に判定する
- JSONLイベント列と最終応答を別ファイルとして扱う
また、--output-last-messageとの比較、--ephemeral有無による保存物の差、別OS、別言語のwrapperは未確認です。これらは今回の成功表へ含めず、採用環境ごとの事前チェック項目とします。
WordPressの変更へ応用する場合は、CodexでWordPressを変更する前の確認項目も参照してください。こちらは検証未完了の計画テンプレートとして、バックアップ、表示確認、復元の条件を整理しています。
採用前に人が見るポイント
自動判定が通っても、そのまま公開や外部更新へつなげません。変更前後を並べて、次を確認します。
- 名前、日時、期限、否定表現など、元の意味が変わっていないか
- 入力にあった内容が欠けていないか
- 入力にない事実や説明が増えていないか
- 対象外ファイルが増減・変更されていないか
- 標準出力、標準エラー、JSONLへ秘密情報が入っていないか
- 同じ失敗を無制限に再実行していないか
失敗時は、入力不備、CLI起動失敗、ターン失敗、コマンド失敗、JSONL構文エラー、成果物不一致のどこで止まったかを記録します。原因または方式を変えない同条件の反復は避けます。

まとめ
codex execをバッチ処理へ組み込むときは、最終メッセージだけで成功を決めず、JSONL、Codex CLIプロセスの終了コード、成果物差分を別々に確認します。特に、エージェント内コマンドが終了コード7でも、Codex CLI自体は0でturn.completedになる実測結果には注意が必要です。
最初は架空データ1ファイルだけの隔離環境で試し、空入力、CLI失敗、壊れたJSONL、コマンド失敗、差分不一致をそれぞれ不採用にできることを確認します。最後は人が意味の変化と対象外変更を見てから採用してください。
資料確認日:2026年10月9日。参照:OpenAI「Non-interactive mode」。Codex CLIの仕様やイベント形式は変わる可能性があるため、導入時と公開直前に公式資料と採用CLI版を再確認してください。
著者・監修者:タケナカ

