📖 はじめに — 理屈より、ここが一番苦労した
パイプラインの設計(第3回)とハーネス(第4回)が固まっても、最後に残るのは「Claude Code のセッション内から、Codex を実装役として安全に動かす」という配管の問題だ。既製の道(MCP・公式プラグイン)が使えず、地雷を 1 つずつ実測で潰すことになった。本記事はそのトラブルシュートの全記録である。
前提環境: Ubuntu 24.04 ベースの Dev Container、Codex CLI(codex exec 非対話モード)、認証はホストの ~/.codex を RW バインドマウント(コンテナ内はブラウザ callback 不可のため、未認証時は codex login --device-auth)。
🔌 MCP を使わない — 消極的選択ではなく設計判断
世のクロスファミリー構成はほぼ MCP か公式プラグイン前提だが、本プロジェクトは codex exec(非対話モード)+ シェルスクリプトで組んだ。理由は 2 つ。
理由 1: 非対話 exec と MCP の組み合わせは sandbox を捨てさせる
実測で判明した連鎖がある。非対話の codex exec では、MCP ツール呼び出しが stdin 途絶で自動キャンセルされる。回避するには sandbox を無効化する --dangerously-bypass-approvals-and-sandbox が必要になる。つまり MCP を挟むと、後述の苦労の末に成立させた bwrap sandbox を自ら捨てる羽目になる。exec + シェルはこの地雷を構造的に踏まない。
これは開発コンテナでは致命的な差だ。開発用コンテナはホスト側リソースへの接続を何かしら持つのが普通で、外側の隔離だけに頼れる構成は稀である。内側の bwrap sandbox(Codex の書込範囲を workspace + /tmp に制限)は手放せない防御線であり、これを無効化するオプションは選択肢に入らない。
理由 2: 決定性と監査性
MCP のブラックボックス委譲では「どの段階で誰が何をしたか」を握れない。シェルで組めば、プロンプト組立・実行・検査・handoff の全段が読める・テストできる・ログに残る。第4回の checksum ガードや自己評価遮蔽は、この全段が自前だから実装できた。
さらに公式プラグイン(codex-plugin-cc)は自動化の向きが逆(Claude 実装 → Codex レビュー = −8.6pt 方向)なので、そもそも採用できない。「MCP を使わない方が、この要件では強い」と言い切れるのは、代替を実装して裏付けたからだ。
🧱 Dev Container で workspace-write sandbox を成立させる 3 条件
Codex に書込を許す --sandbox workspace-write は、コンテナ内では素直に動かない。使い捨てコンテナでの対照実験(条件を 1 つずつ変えた A〜I の系列)の結果、次の 3 条件が揃えば通常ユーザー・sudo 不要で動くことを確定させた。
- apt 版 bubblewrap(
/usr/bin/bwrap) — Codex 同梱の bundled bwrap はパス不一致で不成立。ホスト Ubuntu 24.04 は「/usr/bin/bwrapというパスで exec されたときだけ非特権 userns を許可する」AppArmor プロファイルを標準搭載しており、kernel.apparmor_restrict_unprivileged_userns=1環境ではこれが唯一の合法的通過点 security_opt: apparmor=unconfined(docker-compose.yml) — docker-default プロファイルのままだと bwrap の mount が拒否され、上記プロファイルへの遷移も起きないsecurity_opt: seccomp=unconfined— Docker デフォルトの seccomp が userns 作成と pivot_root を拒否する
対照実験の副産物も 2 つ書いておく。
cap_add: SYS_ADMINは不要だった。過去に SYS_ADMIN を足して失敗した試行の真因は AppArmor 層で、権限の問題ではなかった- sudo 経由の実行は逆に壊れる。bwrap の userns は実行 uid のみをマップするため、root で実行すると一般ユーザー所有の workspace が unmapped になり書けない。「権限が足りないなら sudo」という反射が、ここでは逆方向の罠になる
なお、この 2 つの unconfined はコンテナ外側の隔離を弱めるトレードオフだ。外側と内側どちらの隔離が自環境の脅威モデルで効くかを比較した上で、「内側の bwrap sandbox を機能させる」方を意識的に採った — この判断も決定記録(ADR)に残している。
💣 最大の地雷 — codex exec は sandbox が全滅しても exit 0 を返す
今回の実装で最重要のトラブルシュートがこれだ。
codex execは、内部の全コマンドが sandbox 初期化で失敗しても exit 0 で正常終了する。
agent が例外を飲み込んで「タスク完了」で終わるためで、exit code だけ見ていると「実装成功」と誤認する。実際には 1 行も書かれていない。呼び出し側が Claude Code(自動でハンドオフを進める)だけに、この silent failure は致命的だ。対策は二段。
# 1. bwrap preflight: codex を呼ぶ前に write sandbox の成立を自己診断
if ! bwrap --unshare-user --uid "$(id -u)" --gid "$(id -g)" --bind / / -- true 2>/dev/null; then
echo "❌ bwrap の write sandbox がこの環境で成立しません(preflight 失敗)。" >&2
exit 1
fi
# 2. 出力シグネチャ検査: exit code に加え bwrap エラーの痕跡を grep
CODEX_OUTPUT="$(codex exec --sandbox "${SANDBOX}" --cd "${REPO_ROOT}" "${CODEX_PROMPT}" 2>&1)"
if [ $? -ne 0 ] || printf '%s' "${CODEX_OUTPUT}" | grep -q "bwrap:"; then
exit 1 # 実行環境系失敗 → fallback トリガー
fi
preflight は「壊れた環境で Codex を呼んでから気づく」を防ぎ、シグネチャ検査は「呼んだ後の silent failure」を捕まえる。ツールの exit code を信用できないなら、成立条件を事前に自分で診断し、出力の失敗痕跡を事後に自分で検査する — 泥臭いが、これが実運用の答えだった。
🚦 exit code 意味論 — 運用判断をコードに埋め込む
呼び出し側(Claude Code の Lead セッション)が機械的に分岐できるよう、失敗の「種類」を exit code として定義した。
| exit | 意味 | 呼び出し側の行動 |
|---|---|---|
| 0 | 成功 | レビューブリーフ生成 → Claude verifier へ handoff |
| 1 | 実行環境系失敗(preflight・sandbox・認証切れ) | fallback(Claude 実装へ切替。ただしその実装に Codex レビューは入れない=逆順禁止) |
| 2 | 呼び出し不備(ブリーフ無し・不明オプション・逆順ガード) | fallback せず STOP(呼び出し側の修正) |
| 3 | テスト改ざん検出 | revert + Codex 差し戻し(fallback しない) |
| 127 | Codex CLI 不在 | fallback |
設計上のポイントは 2 つ。
- 1 と 2 の分離。当初は 1 に混在していたが、レビューで「Lead が自分の呼び出しミスを『Codex 不可』と誤判定し、fallback で握り潰すリスク」が指摘され分離した。usage error は環境障害ではない
- 3 は fallback しない。テスト改ざんは契約違反であり、実装役を Claude に替える理由にならない(第4回参照)
exit code は単なるエラー番号ではなく、「この失敗のとき運用はどう動くべきか」の意思決定表になっている。AI エージェントが呼び出し側に立つ時代のシェルスクリプトは、人間向けのエラーメッセージ以上に、この機械可読な意味論が効く。
🔧 トラブルシュート早見表
| 症状 | 原因と対処 |
|---|---|
codex: command not found | CLI 未導入。npm install -g @openai/codex@latest または Dev Container Rebuild(exit 127 → fallback) |
| 認証エラー | ホスト ~/.codex のマウント確認。コンテナ内は codex login --device-auth(ワンタイムコード・ブラウザ不要) |
bwrap namespace / pivot_root エラー | 成立 3 条件(apt bubblewrap + apparmor/seccomp=unconfined)の欠け。Rebuild で反映 |
Can't mkdir ...: Permission denied(sudo 実行時) | sudo が原因。通常ユーザーでそのまま実行する |
could not find bubblewrap on PATH 警告 | write には apt 版 /usr/bin/bwrap が必須(同梱版はパス不一致)。read-only 用途なら無害 |
| workspace-write が legacy 設定で失敗 | 旧 use_legacy_landlock 設定は deprecated かつ書込モード非互換。config から削除 |
| 実装が「成功」したのに変更がない | exit 0 地雷。出力に bwrap: がないか確認。preflight 未実施なら導入 |
🧾 まとめ
- MCP を使わないのは劣化回避ではなく、sandbox 温存・決定性・監査性のための積極的設計判断
- workspace-write の成立条件は「apt 版 bwrap + apparmor/seccomp=unconfined」の 3 点セット。SYS_ADMIN は不要、sudo は逆効果
- 最大の地雷は「sandbox 全滅でも exit 0」。preflight + 出力シグネチャ検査の二段で潰す
- exit code は意思決定表。fallback するか・STOP するか・差し戻すかを機械可読にする
次回は最終回、モデル戦略編。Fable 5・Sonnet 5・Opus 4.8 をどう使い分け、golden set で何を実測し、Sakana Fugu の時代に「買うか組むか」をどう判断するか。
📚 シリーズ記事(ステアリング駆動開発・実践編)
序章
第I部: 技術的負債ドメイン別の実録(総点検ガイド + 8 ドメイン・全 9 回)
- リファクタリング総点検ガイド — 7 つの観点と進め方
- DDD/SOLID/BC 編 — god class 一掃と境界の機械ガード
- SAGA 編 — 新アーキテクチャ挑戦と再発ゲート
- Atomic Design リファクタ編 — 47 page 新規移植
- Storybook × a11y 実機編
- テスト品質・網羅性編
- Python ジョブ群編
- パフォーマンス編
- セキュリティ編
第II部: 検証エンジン(クロスファミリー検証・全 6 回)
- 総論 — マルチ LLM の 2 系統と見取り図
- 裏取り編 — +18.1pt 論文の検証
- 設計編 — finder/verifier 分業と逆順禁止
- TDD×ハーネス編 — テスト保護と三層ゲート
- 実装編(本記事)
- モデル戦略編 — ティア割当と「買うか組むか」
終章
- クロスファミリー検証の最終型 —「正しく作る」から「自分で正しさを測り直す」へ — 検証エンジンに「計測」の層をはめ、自己修正ループを閉じる
番外編
- CI テスト 29 分 → 5.6 分 — 検証エンジンの平時運用実録 — 実測が有力仮説を殺し、退行前より速くなった 1 日
- GPT-5.6 sol 切替の当日実録 — もう一つのデフォルト追従 — CLI 更新が黙って替える finder と、当日中の再計測
- 遊休 90% の枠に仕事を振る — Codex 上流調査と verifier バッチ化 — 逆順禁止の境界を ADR で確定し、読む仕事を遊休枠へ移した 1 日
- ハーネスが単一プロジェクトを卒業する — プラグイン化と二層配布 — 4 プロジェクト展開への切り出しと、version + SHA ピンによる非強制追従
- 69分で5リリース — Opus 5 当日対応が暴いた共通ハーネスの死角 — 新モデル対応を起点に、配布・文書・CI の回帰を二消費者で検出した 69 分
関連記事
- Claude Code 7層ハーネスエンジニアリング — hook・ガード設計の全体像
- Claude Code リモートコントロール — CLI セッションの外部駆動