# OpenQodex 0.11.0 を検証：AI レビュー未完了でも Git push が通るのはなぜか

> Canonical: https://vxi.me/ja/posts/openqodex-prepush-review-fail-open/
> Locale: ja-jp
> Published: 2026-10-11
> Updated: 2026-10-11
> Markdown: https://vxi.me/ja/posts/openqodex-prepush-review-fail-open.md

AI によるコードレビューを Git の pre-push フックに組み込む。考え方自体は自然だ。リモートにコードを送る前にチェックし、レビューを受けていなかったり重大な問題が見つかったりしたら push を止める。そう期待したくなる。

[OpenQodex](https://github.com/openqodex/openqodex) 0.11.0 を、破棄できる Git リポジトリを使って何回か検証した。ShellCheck はわざと作ったシェルスクリプトの不具合を見つけたし、Git フックも設定によっては未レビューの push を拒否した。しかし、独立した AI Reviewer が最後まで処理できなかった場合、フックが必ず push を止めるわけではなかった。

これは OpenQodex の AI レビュー品質についての評価ではない。**AI レビュー自体は最後まで検証できていない。** 重要なのは、静的スキャン、独立 AI Reviewer、そして既存のレビュー記録（Receipt）を参照する push ゲートが別々の仕組みだという点だ。フックを入れただけで、三つとも自動的に終わるわけではない。

## ShellCheck は問題を検出した。ただし本当に動いた場合だけ

まず使い捨ての Git リポジトリに、同じ Shell スクリプトの二つの版を用意した。元の版では変数をダブルクォートで囲み、変更版では意図的に外す。これで ShellCheck の `SC2086` を発生させる。

検証したのは OpenQodex 0.11.0 のスキャナー機能だけで、モデルへの問い合わせは行っていない。中心となる引数は次のとおりだ。

```console
openqodex scan --uncommitted --only shellcheck --block-on-severity nitpick
```

読みやすさのため、実際の検証スクリプトに含まれていた `--cwd`、`--report-dir`、検証済みパッケージへの固定パスは省略している。**実際にこの一行をそのまま実行したという意味ではない。** 再現するなら隔離した Git リポジトリに変更差分を作り、固定版 CLI を使用する。引数の意味は[該当バージョンの公式ドキュメント](https://github.com/openqodex/openqodex/blob/2f2fa9e48c1f62a6123997eb14111140d8e22a74/docs/plumbing.md)で確認できる。

同じ疑似的な変更に対して、ShellCheck を使えるかどうかで結果は変わった。

| 条件 | ShellCheck の状態 | 最終的な検出件数 | 終了コード |
| --- | --- | --- | --- |
| オフライン、インストール禁止 | `not_installed` | 0 | 0 |
| インストールと実行を許可 | `ran` | 1（`SC2086`） | 1 |

これは 2026 年 10 月 10 日の固定版・合成サンプル検証で得た、機密情報を除いた結果だ。オンライン側では `rawCount=1`、`keptCount=1` となり、最終レポートにも `SC2086` が含まれていた。一方、オフライン側では ShellCheck が入っていないため、スキャンそのものが実行されていない。`0 findings` は「問題なし」の証明にはならない。

さらに `SC2086` は、この対応付けでは `nitpick` 扱いになる。OpenQodex の `review.severity_threshold` の既定値は `minor` だ。既定の表示結果だけ見ていると、ルール違反が見つからなかったと勘違いしかねない。以前このフィルタリングに引っ掛かったため、今回の検証では `--block-on-severity nitpick` を明示して、仕込んだ問題が表示されるようにした。

その後、終了コードが 0 かどうかだけでは判断しなくなった。まず**スキャナーが動いたか**。次に、元の検出結果が重大度や変更行のフィルタを通った後どうなったか。ツールが未インストールなのに「検出 0」で終わる場合、この違いは無視できない。

## pre-push フックは毎回 AI を起動するわけではない

次に、Git の push 先をローカルの bare リポジトリにした。実際の業務リポジトリも、ホスティングサービス上のリモートも触らない。

OpenQodex が生成した本物の pre-push フックをインストールし、レビューされていない変更をコミットした。既定設定では、レビューがないという警告は出たものの、Git push は成功し、bare リポジトリのブランチも更新された。

そこで次の設定を追加した。

```yaml
version: 1
review:
  block_on_severity: major
```

新しい未レビューのコミットでもう一度試すと、今度は push が拒否された。テストでは bare リポジトリの `main` が以前のコミットを指したままであることも確認している。単に終了コード 1 を返す自作ダミーフックではなく、OpenQodex の実際のフックを通した結果だ。

| 合成 Git push の条件 | 観測した動作 |
| --- | --- |
| 既定設定で Receipt なし | 警告は出るが push は成功 |
| `block_on_severity: major`、依然として未レビュー | push を拒否、リモート HEAD は不変 |

ここで実証できたのは**未レビューの変更への対応**に限られる。このフックのテストでは Codex を起動しておらず、独立 AI レビューの正常完了を示す Receipt も発行していない。

[固定コミットのフック実装](https://github.com/openqodex/openqodex/blob/2f2fa9e48c1f62a6123997eb14111140d8e22a74/packages/cli/src/commands/hook.ts)を読むと役割が分かる。pre-push は Git が送ろうとしている範囲を調べ、ユーザーの OpenQodex HOME にある変更対応の Receipt を検索し、その結果を使って判定する。`git push` のたびにスキャナーを動かしたり、別の AI モデルを新規起動したりする仕組みではない。

つまり「`scan` が終わった」「`review` が完了した」「`hook` が通した」は三つの別の事実だ。いずれか一つだけで「AI にレビューされた」とは言えない。

## Reviewer が失敗しても push を許可できる理由

三つ目の検証で、より厄介な場面にぶつかった。Mac 上で独立した Codex Reviewer の起動を試すと、サブプロセスはリクエストを開始したが HTTP 401 が返り、OpenQodex は結果を `incomplete` と記録した。正常に完了したレビューの有効な Receipt は得られていない。**したがって AI がバグをどの程度見つけられるか、有効 Receipt や古い Receipt を正しく扱えるかは今回の結果から評価できない。**

その Mac テストの要約には、未レビューの push が拒否された後、`incomplete` の審査を挟んだ別の push が終了コード 0 で成功し、リモートにブランチを作ったという記録もある。ただし、両 push の対象 ref は同一ではない。元の Mac レポートも、第三者が独立に検証できる証拠一式として取り込めていない。この二つをつなげて「審査失敗が push 成功の原因だった」と実験的に証明したとは言えない。

一方、OpenQodex 0.11.0 の**ソースコード上の振る舞い**は確認できる。

固定版の [`push-gate.ts`](https://github.com/openqodex/openqodex/blob/2f2fa9e48c1f62a6123997eb14111140d8e22a74/packages/core/src/push-gate.ts) では、現在の変更と一致し、`kind` が `incomplete` の Receipt を見つけた `checkPush()` は `abstain` を返す。つまり自分から push を止めない。`review.block_on_severity: major` を設定しても、この条件分岐は拒否にはならない。

さらに [`hook.ts` が生成する Git フック](https://github.com/openqodex/openqodex/blob/2f2fa9e48c1f62a6123997eb14111140d8e22a74/packages/cli/src/commands/hook.ts) は、子コマンドの終了コードが 1 の場合にだけ push を拒否する。それ以外のツールエラーや起動失敗は、外側のシェルスクリプトから見ると終了コード 0 になる。レビュー用ツールが壊れたために開発者が Git を使えなくなる事態を防ぐ設計だ。しかし同時に、**重大度のしきい値を設定することと、完全なレビュー成功を必須とすることは別物**だと分かる。

これは運用ポリシーの違いであって、任意の本番コードを迂回できる脆弱性を実証したわけではない。「レビューツールが利用できなければ開発を止めない」という判断もあり得るし、チームでは「レビューできなければ必ず拒否」が必要かもしれない。怖いのは、前者の仕組みを後者だと思い込むことだ。

## 完了した AI レビューを必須にしたいなら

私は現時点では OpenQodex を開発中のフィードバック手段として使い、唯一の強制マージゲートにはしない。スキャナーは違反ルールと対象ファイルを示し、フックはレビュー不足を知らせてくれる。しかしマージ可否は、別途観測可能でテストできる CI チェックに任せたい。少なくとも次の項目を区別する必要がある。

- 必須スキャナーが実行されたか。実際には `not_installed`、スキップ、異常終了ではないか。
- 独立 AI Reviewer が最後まで完了したか。`unavailable` や `incomplete` になっていないか。
- Receipt が現在の commit / diff に対応し、現在のルールで判定されたものか。
- CI や Reviewer が失敗した場合でも、保護ブランチが該当 required check の成功を要求するか。

**これは構成上の提案であって、今回 CI と GitHub ブランチ保護まで統合して実測した解決策ではない。** 全マージに完全な独立レビューが必要なチームは、CI 側の fail-closed 動作を別途検証すべきだ。ローカルの pre-push 設定だけでは、その保証は得られない。

個人リポジトリなら、push のたびにモデルを呼ぶ必要があるとも限らない。必要なときに `review` を実行し、静的スキャンを素早いフィードバックに使い、重要なブランチは検証済み CI で守る。費用と作業の流れの面では、その方が合うかもしれない。「フックを導入した」と「独立 AI が今回の変更をレビューした」を混同しないのが前提だ。

今回確認できた範囲は狭い。OpenQodex 0.11.0 が合成 Shell 差分の `SC2086` を検出したこと。使い捨ての Git リモートに対し、既定では警告のみ、設定後は未レビューの push を拒否するフック動作。固定版のソースには `incomplete` や一部ツールエラーを通す分岐があること。**独立した AI レビューは正常完了していない**ため、モデルの精度、誤検出率、所要時間、有効 Receipt の往復について確かな結論はない。

この未検証部分が残っている以上、OpenQodex のローカルフックだけを本番リポジトリの唯一の強制 AI レビューゲートにはしない。スキャンやレビューの入口として価値はあるが、push を許すかどうかを決める前に、Reviewer が完了できなかったとき誰が止めるのかを確かめる必要がある。
