OSIIX Library

Windows版Codex CLIの実行失敗を切り分ける

Process creation と .git/index.lock で観測した2つの異なる失敗

Type
ケーススタディ・技術記録
Published
2026-09-01
Author
mars70

1. 2つのコマンドが、同じように失敗した

Windows版Codex CLIでコマンド実行に失敗したとき、画面に現れるエラーだけでは、処理がどの段階で停止したのか判断できないことがある。

1つ目は、コマンドを投げても何も起きない。ヘルパーがエラーを返し、PowerShellのコマンド本体は一度も実行されない。

2つ目は、コマンドは実行される。PowerShellが起動し、Gitも起動する。しかし処理の途中で .git/index.lock: Permission denied という拒否が返る。

今回の調査では、表面上はどちらも「コマンドが失敗した」「アクセスが拒否された」ように見える2つの事象を確認した。しかし、ケースAはプロセス作成段階で停止し、PowerShellのコマンド本体まで到達していなかった。ケースBはPowerShellとGitが起動した後、.git/index.lock へのアクセスで停止した。

「エラー文字列を見る」だけでなく、「実行パスのどこで止まったのか」を切り分けることが本稿の中心である。

本稿は、Windows Codex環境で実際に発生した2件の失敗を題材にした実務者向けケーススタディである。新しいサンドボックスアーキテクチャの提案ではなく、意図された保護と実行基盤側の非互換性を、証拠を壊さず切り分ける調査手順に焦点を当てる。

なお本稿における「フォレンジック」は、実行アイデンティティ・認可状態・ランタイムログ・解決された実行ファイル・実装挙動を証拠保存的に再構成する作業を指し、法的な証拠保全手続きやメモリフォレンジックを指すものではない。

2. 問うべきは「なぜ」ではなく「どこまで」

Codexが1つのコマンドを実行するとき、実際には承認、実行アイデンティティ、シェルの解決、プロセス作成、ファイルやディレクトリへのアクセスなど、複数の段階を通る。本稿では失敗位置を整理するため、これを7段階に分けて考える。以下は本稿で使う分析上の失敗帰属モデルであり、Codexの公式アーキテクチャ図ではない。これは、すべての実装が必ずこの順序で各層を通過する実装ステートマシンを表すものではなく、障害位置を切り分けるための診断上の帰属モデルである。

1. ポリシー / 承認
2. 実行アイデンティティ
3. 実行ファイル / シェルの解決
4. プロセス作成
5. リソース認可
6. ツール固有の操作
7. (任意)ポリシー制御下の権限遷移

コマンドが失敗したという事実だけでは、どの段階で問題が起きたのかは分からない。そこで最初に確認するのは、「なぜ失敗したのか」ではなく、「実行パスのどこで止まったのか」である。この問いの立て方の違いが、以下の2つのケースを分けた。

3. 検証条件と関連報告

調査時の環境と設定

以下に、今回の調査で使用した環境と、切り分けのために変更した設定を示す。

設定変更。 切り分けのため、Codex CLI の config.toml にある Windows sandbox 設定を、elevated から unelevated に変更した。

[windows]
sandbox = "elevated"

から、

[windows]
sandbox = "unelevated"

この変更は、elevatedのWindows sandbox実行・セットアップ経路が観測されたhelper errorに関係するかを調べるためのA/B条件であり、unelevatedが正しい恒久解だったと証明されたために行ったものではない。その後のケースAとケースBの観測時にも、設定は sandbox = "unelevated" のままであり、現在もこの設定を継続して使用している。後で述べるように、unelevated への変更だけで本事例全体を説明できるとは確認していない。

ケースAの切り分けでは、後述のPATH実験で、セッションのPATHを変更し、WindowsApps/MSIX版PowerShellと通常インストール版PowerShellのどちらが選択されるかを一変数として比較した。これは実行対象を変える試験条件である。

この設定を採用したことだけで、それぞれのケースの根本原因を説明できるとは確認していない。PATHの変更もPATH自体を根本原因とするものではなく、elevatedが完全なインシデントの根本原因だったとも確認していない。

OpenAIの公式 openai/codex issue trackerには、今回のローカル観測と一部の症状が近いWindows sandbox報告がある。より直接的にケースAと比較できる報告として、Issue #35871は、MSIX/Store版 pwsh.exe が選択されたときに CreateProcessAsUserW がエラー5(Access Denied)を返し、シェルの選択を制御した比較で、WindowsAppsをPATHから外して非パッケージ版のシェルを選ぶと結果が変わったという報告である。これはケースAの実行ファイル選択とプロセス作成の観測に近いが、Issue著者による第三者報告であり、OpenAIのengineeringによる根本原因確認ではない。Issue #35958は、unelevated backendで CreateProcessAsUserW の拒否が -1073283067 と表示される問題を扱い、この値が 0xC0070005、下位16ビットが 0x0005、すなわち ERROR_ACCESS_DENIED に対応することを説明している。ただし、このIssueはエラー表示・デコードの問題に限定され、基礎にあるsandboxの根本原因を証明するものではない。

別の関連報告として、Issue #33388は、elevated Windows sandboxで SetNamedSecurityInfoWC:\ProgramData\Temp に対して 5 を返し、sandbox = "unelevated" へ切り替えると通常のsandboxコマンドが実行できたため、unelevatedをworkaroundとして記録している。Issue #27889は、Codex Desktop 26.609の更新・再インストール後に、WindowsAppsのパッケージディレクトリでelevated sandboxのセットアップとACL操作が SetNamedSecurityInfoW failed: 5 となり、helper_unknown_errorやACL読み取りにもエラーが出たという報告である。これらはACLセットアップ時の報告であり、unelevated環境で CreateProcessAsUserW が失敗した本稿のケースAとは、sandboxモードと失敗したAPIが異なる。

これらはいずれもOpenAIの公式issue trackerに掲載された第三者報告であり、今回のローカル観測と症状の類似を補強する外部の補足証拠である。ただし、いずれもOpenAIのengineeringによる根本原因確認ではなく、ローカル観測と同一の根本原因を証明するものでもない。

4. ケースA — 子プロセスが一度も起動しない

投入されたコマンドは汎用的なヘルパー/プロセスエラーで失敗した。sandbox logには CreateProcessAsUserW failed: -1073283067 と記録されていた。PowerShellのコマンド本体まで到達していなかった。構文、コマンド長、Gitの挙動、リポジトリ状態、ヘルパーの不安定性など、複数の仮説がありえた。

ランタイムログを見ると、より正確な境界が現れた。選択された実行ファイルは次のMicrosoft.PowerShell MSIXペイロードであり、

C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.5.0_x64__8wekyb3d8bbwe\pwsh.exe

CreateProcessAsUserW は、要求されたPowerShellコマンド本体が実行される前にアクセス拒否を返した。観測されたエラー値の一つは -1073283067(0xC0070005)で、下位のWin32コードは 5、すなわち ERROR_ACCESS_DENIED に対応する。

Codexソースおよび公開されているOpenAIのWindowsサンドボックス技術資料によれば、Windows環境の実行境界は制限付きプライマリトークンを構築し、CreateProcessAsUserW 経路で子プロセスを起動する。この事実により、インシデントは「コマンド失敗」から「プロセス作成段階の認可失敗」へと再分類された。

対象パッケージ自体は存在し、状態も正常だった。読み取り専用の調査で確認できたのは、失敗がエイリアス欠落や実行ファイル不在によるものではないという点である。パッケージ破損や欠損ファイルという説明は支持されなかった。

次に、変数を1つだけ変える実験を行った。シェル解決の優先順位を変更し、MSIX版ではなく通常配布版のPowerShell(C:\Program Files\PowerShell\7\pwsh.exe)を選ぶようにしたところ、MSIX版で失敗していたコマンドは正常に起動した。WindowsAppsの解決が再び優先されると、失敗も再現した。実行対象の解決先が、同一のホスト・リポジトリ状況下でも結果を左右することが分かった。この結果は、サンドボックスの無効化やWindowsApps側のセキュリティ変更ではなく、通常配布版PowerShellを優先するという狭い運用回避策を選ぶ根拠になった。

ただし、この時点では通常のACL確認だけでは足りなかった。WindowsApps およびパッケージディレクトリ、実行ファイルのACLは、NT AUTHORITY\RESTRICTED を含む主体に対して通常のRead/Execute権限を示しており、「制限トークンが単に実行権限を持っていない」という単純な説明を弱めていた。生のSDDLを確認すると、パッケージ識別を条件とするアクセス制御条件式(conditional ACE)が現れた。

WIN://SYSAPPID Contains "Microsoft.PowerShell_8wekyb3d8bbwe"

これは、MSIX実行面がパッケージアイデンティティを条件とする認可セマンティクスを持つことを示す証拠である。ただし、この条件付きACEが、失敗した CreateProcessAsUserW 呼び出しにおける最終的な拒否条件そのものであったことまでは証明していない。

以上を総合すると、評価は次のようになる。選択対象が登録済みMSIX版PowerShellだったこと、失敗がコマンド実行前に発生したこと、非MSIX版では局所的に回避されたこと、パッケージ/実行ファイルのSDDLにWIN://SYSAPPID条件ACEが含まれていたことは、いずれも確認済みである。ファイル欠損・パッケージ破損、および単純な読み取り/実行権限拒否という説明はいずれも支持されない。

一方、制限トークンによるプロセス作成が、この環境で選択されたMSIX/パッケージ認識型の実行面と非互換的に相互作用しているという解釈は、強く支持されるが確定はしていない。ERROR_ACCESS_DENIED を最終的に返したWindows内部のアクセスチェック構成要素そのものは、未解決のまま残っている。

この未解決事項は、今回必要だった運用判断——非MSIX版PowerShellを優先するという回避策の採用——には影響しなかった。そのため、それ以上の低レベルな内部解明は行わなかった。これは「未解決事項は重要ではない」という一般論ではなく、この特定の判断において追加調査が不要だったという、狭い範囲の結論である。

5. ケースB — プロセスは起動するが、保護されたリソースが書き込みを拒否する

別のインシデントでは、Gitのステージング操作が次のエラーで失敗した。

.git/index.lock: Permission denied

ここで取りうる、しかし安全ではない対応として、ACLエントリの削除、リポジトリ所有権の変更、常時昇格実行、あるいは「解決できないSIDは古い残留エントリだろう」という推測があった。これらは証拠収集の段階では実行しなかった。

観測できたのは、PowerShellが起動し、Gitも起動したという事実である。失敗が発生したのは、Gitが .git/index.lock を作成しようとした時点だった。つまりこのインシデントは、プロセス作成の失敗ではなく、ファイルシステム/リソース認可の領域に属していた。

子プロセスの TokenRestrictedSids を読み取り専用で調査すると、リポジトリのACLにも現れる合成的なcapability SIDが同一のものとして見つかった。実効的な関係は次のとおりだった。

リポジトリルート
  <workspace-capability-SID> -> Modify許可

.git
  同一のSID -> 書き込み/削除拒否

同じcapability SIDは、Codexのサンドボックスが保持する永続化状態を通じて、アクティブなワークスペースに紐づいていた。

ここで注意すべき点がある。通常のWindowsアカウント名解決では、いくつかの合成的な S-1-5-21-* エントリについて、対応するフレンドリー名が得られなかった。この事実だけを見れば「解決できないエントリ=孤立した古いACL」と誤分類しかねない。しかし、この解決できないSIDの1つは、同時にアクティブな子プロセストークンの制限SID集合に含まれ、リポジトリルートにModifyアクセスを許可され、.git には書き込み/削除系アクセスを明示的に拒否され、永続化されたCodexのcapability状態を通じてアクティブなワークスペースに紐づいていた。ここから導かれる原則は、名前解決の失敗は、活動性や有効性の検査ではない、ということである。合成的なセキュリティアイデンティティは、通常のアカウント照会では意図的に解決不能なままであることがある。

OpenAIが公開しているWindowsサンドボックスの技術資料は、.git を、それ以外は書き込み可能なワークスペース内で保護される対象パスの一つとして明示的に説明している。同資料は合成SIDと書き込み制限トークンによってワークスペース単位の書き込みを強制する仕組みについても説明している。この文書化された実装と、今回観測したアクティブな制限capability SID・リポジトリルートのAllow ACE・.git のDeny ACE・永続化されたcapabilityマッピング・実際の index.lock 拒否という一連のローカルでのフォレンジック確認を合わせることで、その保護機構がこのマシン上でどう働いていたかを再構成できた。

同じGit操作は、承認/昇格された実行経路を通せば成功した。ソースコードおよびOpenAIの資料は、サンドボックスと承認ポリシーを別個の制御として説明している。サンドボックスが技術的な境界を定義し、ポリシーがその境界を越えられるかどうかとその方法を決定する。

通常のサンドボックス権限
        ↓
保護境界が操作を拒否
        ↓
ポリシー制御下の権限遷移
        ↓
より高い権限での再試行

本稿は、すべての権限遷移が常に人間の手動承認を必要とすべきだと一般化するものではない。重要な性質は、その遷移が明示的であり、ポリシーによって制御され、通常の権限階層と区別可能であることである。

まとめると、子プロセスとGitが起動したこと、拒否が .git/index.lock の作成時点で発生したこと、アクティブなcapability SIDが制限トークンとACLの両方に一貫して現れたこと、.git の書き込み保護がOpenAIによって明示的に文書化されていること、承認/昇格経路では成功したこと——これらはすべて確認済みである。一方、index.lockの残留、リポジトリの破損、「解決不能なSID=孤立アカウント」という解釈は、いずれも支持されない。運用判断としては、.git の保護をそのまま維持し、ACL境界を弱めるのではなく、ポリシー制御下の高権限経路を利用することが選ばれた。

6. なぜ表面上は同じ失敗に見えたのか

2つのケースを並べる価値は、外形上の症状が似ているのに、停止位置がまったく異なっていた点にある。

ケースA
実行ファイル解決 → プロセス作成 → 拒否
子コマンドは一度も開始しない

ケースB
実行ファイル解決 → プロセス作成 → 成功
リソース認可 → 拒否
Case AはProcess creationで停止し、Case Bは子プロセス起動後のリソース認可で停止する、停止位置の比較図
図1. Case A と Case B における停止位置の違い。Case A はプロセス作成段階で停止し、Case B は子プロセス起動後のリソース認可段階で停止した。

表面上はどちらも「コマンドが失敗した」という同じ現象に見えたが、実際には異なる実行段階で停止していた。本稿の教訓は、新しいサンドボックスアーキテクチャを提案することではなく、失敗が実行パスのどこで起きたのかを切り分けることにある。

7. 証拠を保存したまま調査する

この2件の調査では、失敗位置を切り分けるため、次の7段階で確認を進めた。

1. 最後に成功した層を特定する

子プロセスが作成されたかどうかをまず確認する。

  • 子プロセスが作成されていなければ、実行ファイル解決・実行アイデンティティ・プロセス作成API・パッケージアイデンティティ・起動ポリシーを調べる。
  • 子プロセスが作成されていれば、終了ステータス・リソース認可・ツール固有のセマンティクス・権限昇格の挙動へ進む。

この一つの分岐が、ケースAとケースBを一つの診断に混同させなかった。

2. 是正する前に環境を保存する

ACL、所有権、サンドボックス状態、パッケージのインストール状態、システムのセキュリティポリシーを変更する前に、解決された実行ファイルの正確なパス、プロセス起動結果、トークンのアイデンティティと制限SID、ACLと生のSDDL、サンドボックスのcapability状態、ランタイムログ、関連する実装のソース、ベンダー資料、承認/昇格経路を確保する。

3. 一度に1つの変数だけを変える

ケースAで有効だった実験は、シェル解決だけを変えることだった。パッケージの再インストールや権限の一括リセットは、より解釈しにくい結果しか生まなかっただろう。診断的な実験は、成功しても失敗しても情報を与えるものであるべきである。

4. 必要なら一段深いセキュリティ情報を見る

ケースBでは whoami /groups は決定的なcapability SIDを明らかにせず、TokenRestrictedSids を直接調べることで判明した。ケースAでは Get-AclAccessToString はパッケージ認識型の条件を表示せず、生のSDDLがそれを明らかにした。

通常の確認 不足した情報 追加確認
whoami /groups 決定的なcapability SID TokenRestrictedSids
Get-Acl / AccessToString パッケージ認識型の条件 生のSDDL

教訓は使いやすいツールを放棄することではなく、その抽象化が調査対象の事実を隠している場合には一段深く潜ることである。

5. 複数の証拠を突き合わせる

ランタイムトークン、ACL/SDDL、永続化されたcapability状態、ログ、実装ソース、ベンダー資料、制御されたA/B挙動——同一の仕組みが複数の証拠源から再構成できるほど、結論は強くなる。単一の証拠源だけで完全な解釈が確立することはない。

6. 意図された拒否と実装上の問題を分ける

ACCESS_DENIED は複数のカテゴリを表しうる。ケースBは意図された文書化済みの保護境界であり、ケースAは選択されたパッケージ実行面と制限付きプロセス作成経路との実行互換性の問題として強く支持された。エラーコード単体は両者を区別しない。

7. 証拠が尽きたところで止める

成熟した調査は、確認できたこと・強く支持される仮説・棄却されたこと・未解決のことを明確に記録する。目標はすべての未知を消すことではなく、必要な判断を下せる程度まで不確実性を減らすことである。

8. 避けるべき誤認

「アクセス拒否=権限を修正すべき」という直感は、その拒否が意図された制御である可能性を見落とす。「解決できないSID=古い残留ACL」という推測は、合成capabilityアイデンティティが意図的に解決不能な場合があることを見落とす。「同じコマンドが失敗した=同じバグ」という判断は、一方がプロセス作成前に、もう一方が子ツール起動後に失敗しているという違いを見落とす。「昇格実行すれば動く、だから昇格が正しい対処だ」という判断は、有効な切り分け手段ではあっても、成功したからといって権限を恒久的に広げる根拠にはならない。そして「回避策が機能した=根本原因が証明された」という判断も誤りである。ケースAの非MSIX版による回避策は、実行ファイル/パッケージ選択が原因の引き金であることを切り分けたに過ぎず、Windows内部の最終的な拒否条件そのものを証明してはいない。

9. エージェント実行基盤の可観測性で見るべき点

証拠保存的な診断は、ランタイムがセキュリティ経路そのものを直接可視化していれば容易になる。どの実行ファイルが解決されたか、どの実行アイデンティティが使われたか、プロセスは作成されたか、どのリソース境界が拒否したか、ツール自体がエラーを返したのか、より高い権限が検討または試行されたか——これらに答えられることが、今回のケースから得られる実務上のポイントである。

具体的には、次のような情報が望ましい。

  • 解決された実行ファイルパスと、関連するパッケージ/アプリケーションアイデンティティ
  • 解決失敗・プロセス作成拒否・リソース認可拒否・ツール終了失敗・承認拒否・高権限再試行失敗を区別できる失敗分類
  • 合成SIDを運用者が逆解析しなくて済むようなcapabilityの可視化
  • 書き込み可能なワークスペース内でも保護され続けるリソースクラスの明示
  • 通常の実行階層から高権限経路へ処理が移った時点と理由を再構成できる記録

失敗時に閉じて不透明なヘルパーエラーだけを返すシステムは、権限を静かに広げるシステムよりは安全だが、どの層で拒否されたかまで示せるシステムには運用上の強さで劣る。

10. この事例が証明していないこと

本事例は、すべてのAIエージェント用サンドボックスがWindowsの制限トークン、合成SID、NTFSのDeny ACEを使うべきだと証明するものではない。.git が常にエージェントから保護されるべきだと証明するものでも、すべてのMSIXアプリケーションが制限付き実行と非互換であると証明するものでも、すべての権限遷移が手動承認を必要とすべきだと証明するものでもない。新しい多層アーキテクチャを確立するものでも、根底にあるセキュリティ原則に学術的な新規性を主張するものでもない。一般化できる範囲はより狭く、AIエージェントのツール実行は複数の実施層を横断しうること、失敗はそれが発生した層に帰属させるべきであること、意図された保護と実行上の非互換性は表面上似た拒否症状を生みうること、そしてそれらを効率的に区別するにはランタイムの可観測性と証拠保存的な調査手順が必要であること——これが本事例から言えることである。

11. 結論

調査は、当初は繰り返し発生するWindowsヘルパーの不安定性という漠然とした問題として始まった。しかし実際には、性質の異なる2つのセキュリティイベントに分解できた。一方は、子プロセスが作成されず、プロセス作成段階で停止したことが確認された失敗だった。MSIX版と非MSIX版の実行ファイル選択で結果が変わるA/B挙動も確認されたため、制限付き実行経路と、この環境で選択されたパッケージ化された実行面が非互換的に相互作用したという解釈は強く支持される。ただし、ERROR_ACCESS_DENIED を最終的に返したWindows内部のアクセスチェック構成要素は未解決である。もう一方は、ワークスペース単位の制限アイデンティティが .git の書き込み保護と衝突し、通常のワークスペース編集は許可される一方でGitメタデータの書き込みは拒否され、ポリシー制御下の高権限経路では同じ操作が成功した失敗だった。

この2件からまず確認すべきなのは、「何のエラーか」だけではなく、「実行パスのどこで止まったのか」である。ACCESS_DENIEDだけでは、プロセス作成段階の拒否と、PowerShellやGitの起動後に発生した .git のリソース認可拒否を区別できない。意図された保護境界と実行上の非互換性を分け、確認済みの事実、強く支持される解釈、未解決の点を別々に扱う必要がある。

本稿の貢献は、新しいAIエージェント向けセキュリティアーキテクチャではない。ここで扱ったセキュリティ制御そのものは、既存の現代的なサンドボックス設計に属している。貢献は、実行パスを再構成し、最後に成功した層を特定し、環境を保存し、実効的なアイデンティティと認可データを調べ、ローカルの証拠をソースコードとベンダー資料と相互参照し、変数を一度に1つずつ変え、意図された制御と非互換性とを区別し、証拠の限界で止めるという、証拠に基づく失敗の再構成そのものにある。この切り分け方は、Windows版Codex CLIに限らず、複数の実行・認可段階を持つエージェント実行基盤にも応用できる。何のエラーかだけでなく、どこで止まったのかから調べることが、制御を弱めずに安全な運用判断を支える。

再現・証拠チェックリスト

類似した事象を再現または調査する場合は、是正より先に次の情報を記録する。

  • 解決された実行ファイルまたはシェルのパス
  • 子プロセスが実際に起動したかどうか
  • 実効トークンのアイデンティティと制限SID。必要に応じて TokenRestrictedSids を含める
  • 対象リソースのACLと生のSDDL
  • sandboxおよびランタイムのログ
  • 該当する場合は、永続化されたcapability状態とワークスペース状態
  • 制御したA/B条件と、該当する場合は承認・昇格経路の結果

これは証拠を収集するためのチェックリストであり、ACL、所有権、sandboxの保護境界を変更する手順ではない。

References

参考資料

最初の2件は、文書化されたアーキテクチャの位置づけと新規性の過大主張回避のために用いた。Issue #35871と#35958はケースAに近い外部報告として、#33388と#27889はWindows sandboxのACLセットアップに関する関連報告として、いずれもインシデント固有の分析を支える補足的な証拠として扱っている。