verify-bootstrap — repo に機械ゲートを立てる
人間がコードを読む速度は、AI が書く速度に追いつかない。だから構造的な正しさの判定は 機械に移す。この skill は repo ごとに、 その時点で最良の検査ツールを立てる。
この skill が持たないもの(設計上の中核)
ツール名の一覧を持たない。言語の検出テーブルも持たない。 ツールは陳腐化する (flake8 + black + isort → ruff は 2 年で起きた)。固定表を持てば表が腐り、 腐った表が repo に配られる。
持つのは 調べ方と契約だけ:
- repo の実態を列挙する手順(静的な言語リストを参照しない)
- category ごとに何を search-first に問うか
- 生成物
.claude/verify.shの契約(global hook がこれだけを知る) - strictness と陳腐化検知の規律
結果として、この skill が書かれた時点で存在しなかった言語・ツールでも同じ手順で回る。
モード
| モード | 起動条件 | やること |
|---|---|---|
| bootstrap | .claude/verify.sh が無い |
Step 1–6 を通す |
| audit | 既にある(--audit / 「棚卸し」) |
Step 1 を再実行し、.claude/verify.md の選定を search-first で引き直して差分を提案 |
Step 1 — repo の実態を列挙する(検出、静的表なし)
推測しない。実測する。
# 追跡ファイルの拡張子分布(上位)— 生成物・依存ディレクトリは除く
git ls-files | sed -n 's/.*\.\([A-Za-z0-9_]*\)$/\1/p' | sort | uniq -c | sort -rn | head -20
# ビルド/依存マニフェストの実在
git ls-files | grep -iE '(^|/)(pyproject\.toml|package\.json|Cargo\.toml|go\.mod|Gemfile|pom\.xml|build\.gradle.*|\*\.xcodeproj|Package\.swift|mix\.exs|composer\.json|Makefile|justfile)$'
# 既存のゲート痕跡(重複導入の防止)
git ls-files | grep -iE '(pre-commit-config|lefthook|\.github/workflows/|trunk\.yaml|\.golangci|\.eslintrc|ruff\.toml)'
上のマニフェスト grep は発見の補助であって権威ではない。1 本目の拡張子分布が正本で、 grep に載っていない生態系が出てきたらそれを Step 2 にそのまま渡す(表を編集して この skill に足さない — 表を持たないことがこの skill の設計)。
散文・設定・スキーマも対象に含める。コードだけがゲートの対象ではない (Markdown の日本語 prose、YAML、CI 定義、シェルスクリプトはいずれも機械検査できる)。
Step 2 — category ごとに現時点の最適ツールを調べる
検出した生態系ごとに、6 category を埋める。埋まらない category は「無い」と明記する (空欄と「その言語には該当がない」は別物)。
| category | 問い |
|---|---|
| format | 整形の正規化。差分ノイズを消し、レビュー対象を意味の変化だけにする |
| lint | 構造的な誤り・複雑度・デッドコード |
| type check | 型で表現できる契約。型なし言語なら「型注釈を段階導入する手段があるか」 |
| security | 危険な構文・秘密の混入(秘密スキャンは harness 側にもあるので重複を確認) |
| dependency | 既知脆弱性・未使用依存・ライセンス |
| test | テストランナーとカバレッジ測定 |
予算系 rule(閾値を要する複雑度・関数/ファイル長・bundle サイズ等)は既定に頼らない。
主要 linter は予算系を既定 OFF で出荷するのが通例(閾値が opinionated なため)—
「最大 strict」の運用では構造的に素通りする(2026-08-28 実測でカバー 0 を確認 —
数値の正本は ADR-0056)。lint category を埋めるとき明示的に問う。stack の標準 toolchain が
既定で予算を持つ場合(例: SwiftLint の cyclomatic_complexity / file_length)は追認して
verify.md に 1 行記録するだけでよい。閾値は global に定めない — repo 間で分布は数倍
割れる(同実測、正本は ADR-0056)。
既存 corpus 全件の分布を実測してから、現状の外れ値だけが赤くなる位置に置く
(skill review-to-lint の免除境界の原則。ゲートを初日に赤くする閾値は設計ミス)。
lint / type check の選定方針は LLM-first(読者・編集者とも LLM で、人間はコードを
読まない前提。2026-08-31 の harness 棚卸しで確立 — 実例と根拠は
~/.claude/.claude/verify.md の同日付節)。言語を問わず同じ 4 軸で select を組む:
- バグクラス検出(未定義名・未使用・既知の危険パターン・型矛盾)— 無相関の 検証者の中核。最優先
- 正準形(formatter・import 順)— 目のためでなく diff 安定のため。別セッションが 触っても偽 diff が出ない = golden・review が静かに保てる
- 予算(複雑度・ファイル長)— context 制限つきの次の編集者のための編集信頼性予算 (閾値は上の分布実測で)
- 境界の明示型強制 — 型は機械検証可能な仕様で、再生成・別セッション編集の凍結端。
production 側のみ(テスト関数は消費される境界ではないので除外)。
Python での実装例: ANN001-003 + ANN201 + ANN401、
tests/**除外
人間美学系(docstring 様式・命名規約・コメント整形)は select しない。 削る対象では なく、最初から入れない。「人間が読みにくい」だけを理由とする rule には LLM-first の 正当化が立たない。
各 category について search-first skill を呼ぶ(WebSearch を直接叩かない —
依存追加・自作 utility の前は search-first、が rules/common/planning.md の配線)。問いには必ず
「今日の日付時点で」の時点指定と「既存の代替から乗り換えが起きていないか」を含める。
記憶している定番を答えにしない — この skill が防ごうとしているのはまさにそれ。
Verdict が出たら、次を確認してから採用する:
- 最終リリースが 12 ヶ月以内か(放置プロジェクトを新規 repo に入れない)
- 単一バイナリ / on-demand 実行が可能か(
uvx/npx/brew等。repo の 実行環境を汚さずに走るか) - 設定を持ち込めるか(strictness を宣言でき、例外を許可リスト化できるか)
- CI とローカルで同じものが走るか(乖離するとローカルが形骸化する)
Step 3 — 最大 strict で導入する
既定は最大 strict。AI は厳しさに文句を言わないので、従来「人間の忍耐」を理由に 緩められていた分の天秤は正しさ側に倒す。
- 警告を warning のまま放置しない — error にする
- 型の抜け穴(
any相当・型チェック抑制コメント)を 禁止側に倒す - 版を pin する(
ruff==0.16.0のように。supply chain の固定。bump は手動) - 例外はインラインの抑制コメントでなく、設定ファイルの許可リストで理由付きに (インライン抑制は理由が消えて無限増殖する)
新規 repo は初日から最大 strict(drain すべき負債がゼロなので ratchet 不要)。 既存 repo への後付けだけ ratchet を使う — 全 rule を warn で入れ、既存違反を drain し切ってから error に上げる。初日から block にすると「回避の作法」が育つ。
予算系 rule の閾値も同じく一方向 — 超過したら閾値を上げるのではなく刈る (dead code・重複の除去、分割)。閾値の設定行に「上げずに刈る — 変更は verify.md に 日付つき理由」のコメントを付ける。ゲートが赤くなった瞬間に config を触る agent の 目に入る位置が、この運用規約の配達点である。
Step 4 — .claude/verify.sh を生成する(global hook との契約)
repo が持つ 唯一の入口。ハーネス側の hook はこのファイルの存在と exit code しか 見ない — だから repo が何語で書かれていても hook を変更せずに済む。
契約(厳守):
| 項目 | 仕様 |
|---|---|
| パス | <repo root>/.claude/verify.sh(実行可能) |
| 引数 | --staged = commit 境界の高速検査(staged ファイルのみ)。引数なし = repo 全体の完全検査 |
| 環境 | $VERIFY_REPO_ROOT があれば repo root として使う(承認機構が渡す) |
| exit code | 0 = PASS / 1 = FAIL(commit を止める)/ 2 = 検査不能(ツール不在等、fail-soft) |
| 出力 | FAIL 時は人間と LLM が読んで直せる検出行。PASS 時の出力は advisory(commit は止めないが伝えたいこと — 昇格待ちの ratchet、眠っているゲートの通知)として verify-precommit.sh が model へ渡す。言うことが無ければ無出力(無言 PASS にノイズを足さない)。stdout と stderr は区別されない — hook は 2>&1 でまとめて受けるので、成功時に stderr へ出る進捗・非推奨警告も advisory になる。黙っていたいものは黙らせる |
| 実行時間 | --staged は数秒以内。超えるものは無引数側にだけ置く |
速い / 遅いの分離(これを守らないと bypass の作法が育つ):
--staged: format check・lint・security scan(ファイル単位で完結するもの)- 無引数: build・type check・test・dependency audit(repo 全体・分単位)
ツール解決は PATH にあれば使う → 無ければ on-demand 実行(uvx / npx 等)→ それも 無ければ exit 2 で fail-soft。fail-soft でも無音にしない — どのゲートが眠って
いるかを stdout に出す(眠っているゲートは、無いゲートより危険)。
実装上の罠(どちらも実測で踏んだもの):
repo root は
$VERIFY_REPO_ROOT→ 自分自身の位置、の順で決める — cwd 起点にすると hook 経由で別 repo を検査して無言で PASS する fail-open になる。承認機構は照合済み バイト列を一時ファイルに置いて実行する(TOCTOU 回避)のでBASH_SOURCEが repo を 指さない場合がある。両対応が必須:if [[ -n "${VERIFY_REPO_ROOT:-}" ]]; then ROOT="$VERIFY_REPO_ROOT" else ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P) || exit 2 fi追跡されているか確認する —
.gitignoreが.claude/*を除外している repo は多い。 除外されたままだと CI と他クローンからゲートが消える。git check-ignoreで確認し、 必要なら!.claude/verify.sh/!.claude/verify.mdを足す
生成しただけでは commit 境界で走らない — hook は permission プロンプトを経ずに実行するため、 「ファイルがある」を「実行してよい」と読み替えない設計になっている(clone しただけの外部 repo で コードが自動実行される経路を塞ぐ)。人間が内容を読んで承認して初めて実行される:
python3 ~/.claude/scripts/hooks/verify_allow.py approve <repo>
承認は内容ハッシュに紐づくので、verify.sh を編集したら verify_allow.py の台帳を更新する前に
新しい本文を確認する。これは permission prompt を経ない repo-local code 実行の信頼境界である。
生成した verify.sh はその場で 1 回実行して動作を確認する。さらに
違反を一時注入して category ごとに発火を実証する(format 崩し・未使用 import・
eval 等)。PASS するだけの確認は、ゲートが眠っていても PASS するので証拠にならない。
実証が済んだら注入した probe を必ず削除する。
Step 5 — 選定を記録する(陳腐化を可視化する)
<repo root>/.claude/verify.md に、category ごとに次を残す:
## type check
tool: pyright ==1.1.x
選定日: 2026-07-31
理由: mypy より速く既定が strict。ty / pyrefly は 2026-07 時点で pre-1.0
再調査トリガー: 12 ヶ月経過 / 代替が 1.0 到達 / 現行ツールの最終リリースが 12 ヶ月以上前
この記録が無いと audit モードが「何を引き直すべきか」を判断できない。
選定日と再調査トリガーは必須、理由は 1 行でよい。予算系 rule は閾値と実測分布の
as-of も記録する(例: C901=15 — p99=14、2026-08-28 実測。audit 時に分布の再実測と
突き合わせる基点になる)。
Step 6 — CI に同じものを配線する(任意だが推奨)
ローカルゲートだけでは、bypass された commit が素通りする。CI が .claude/verify.sh
を無引数で実行する job を持てば、ローカルと CI の乖離が構造的に起きない
(同じ入口を両方から呼ぶ。CI 用に別のコマンド列を書くと必ず drift する)。
audit モード
- Step 1 を再実行(stack が変わっていないか — 言語が増えていれば category が空く)
.claude/verify.mdの各 category について、再調査トリガーに該当するものだけ search-first で引き直す(全件引き直すと毎回コストが出る)- 差分を提示: 現行 → 候補、乗り換えコスト、据え置きの理由
- 判断はユーザー。自動で乗り換えない — ツール変更は repo 全体の diff を生み、 元に戻すコストが導入コストを上回る
アンチパターン
- 固定のツール表をこの skill に足す — 表が腐り、腐った表が全 repo に配られる。 Step 1–2 を回すコストを惜しんで表を作った時点でこの skill は死ぬ
- インラインの抑制コメントを許す — 理由が消え、例外が無限に増える
- 警告を warning のまま運用する — 誰も見ない。error にするか、消すか
- repo 固有の特殊ルールを積む — 「この repo だけ独自」は設計不足のサイン。 まずスタックを標準に寄せ、それでも要る例外だけ理由付きで許可リストへ
- 遅い検査を
--stagedに入れる — commit のたびに数十秒待たされ、bypass が常態化 - 生成して実行せずに終える — 動作未確認のゲートは、無いゲートより悪い(あると誤認する)
関連
- Phase 0 のエントリポイント: skill
search-first(ツール選定は必ずここを通す) - ゲートを含む実装フロー全体: skill
implementation-chain