PlanGate

Hook Enforcement — runtime で強制すべき不変条件

Status: v5(Implementation: 10/10 hooks Done — #169 完走 — #157 で 3 hook、#169 セッション A で 2 / B で 3 / C で 2 = 計 10 hook、3 mode 設計) Hook 数の現状(v8.7.0 以降): 本書は v8.5.0 時点の 10/10 hooks スナップショット。 v8.6.0 で EH-8check-metrics-privacy.sh、metrics privacy 強制)、 v8.7.0 で EH-9check-delegation-commit-boundary.sh、TASK-0073 F2)を追加し、 現状は EH-1〜EH-9 + EHS-1〜EHS-3 = 12/12(+ EH-12 / EH-13(追加分・別記、下記注記参照))。本書本文の表は v8.5.0 構成のまま 維持し、追加分の詳細はそれぞれの実装 PR / CHANGELOG / bin/plangate doctor 出力を参照。

EH-12(追加・配線は Human apply 待ち): protected branch 上の破壊的 git 操作 block(check-git-destructive.sh)。hook 本体(非 HO の scripts/ 直下)は 実装・テスト済みだが、PreToolUse 配線は scripts/apply-eh-git-destructive-guard.sh --applyHuman-owned)の実行後に 有効化される。番号は EH-10 / EH-11 が既に予約済み(#760 PostToolUse 軽量品質 チェック / #762 Stop 軽量 verify の「候補」名として .claude/settings.example.json のコメントで使用、 かつ EH-10 は docs/rfc/ai-self-set-gate-hook-enforcement.md の RFC Draft が保持)のため、衝突しない最小の空き番号として EH-12 を採番した。

EH-13(採番のみ・配線済み): 承認トークン直書き block (scripts/check-approval-token-write.sh、TASK-0123 で導入・TASK-1023 #1023 で fail-closed 化)。settings-wiring-contract.md 旧記載の「EH-10」は上記予約 (#760 / #762)と衝突していたため、TASK-1023 G-6(Human 裁定 2026-08-10)で 衝突しない最小の空き番号 EH-13 へ改番した。

実装と物理配線の区別(2026-06-10 棚卸し / 2026-06-27 更新): 12/12 は 「スクリプト実装 + 単体テスト済み」を指す。発火経路(settings.json / .codex/hooks.json / CI / bin/plangate)への物理配線は 11/12(PreToolUse/CI 配線 6

⚠️ 上記の「物理配線」は Claude Code 側(.claude/settings*.json)を数えた値であり、 .codex/hooks.json を配線済みとして数えてはならない(#1078 実測 2026-08-13): .codex/hooks.json は top-level の仕様外キーにより JSON 全体が parse 拒否され、 Codex 側の hook 登録は 0 件hooks/list 実測)。EH-1/2/3/6/9 は Codex セッションで一度も発火していない。本注記の「発火経路」列挙に .codex/hooks.json を含めるのは現時点では誤りである。 正本: settings-wiring-contract.md §Codex CLI parity。 一般則: 配線の記述件数を「配線済み」と数える運用は、今回と同型の silent failure (設定は正しいがランタイムが受理していない)を見逃す。ランタイム側の登録状態を 問い合わせた証跡を伴って初めて配線済みと数えること。

発火条件の供給(EPIC #527 follow-up・配線済み): EHS-1/2/3 の発火条件 PLANGATE_VALIDATION_BIAS=strict は、bin/plangate verify / handoff --verify--profile <key> を受理し model-profiles.yamlvalidation_bias を解決して内部 export する(TASK-0147 / #644 で配線・適用済み)。env で明示注入済みなら尊重し、 normal/lenient profile・無指定では非発火(既存挙動不変)。未知 key / yaml 欠落時は normal fallback + stderr 警告(scripts/_resolve_validation_bias.py)。

配線状態 Hook 発火経路
✅ 配線済み(6) EH-1 / EH-2 / EH-3 / EH-6 / EH-9 Claude PreToolUse + Codex hooks.jsonCodex 側は登録 0 件・未発火 / #1078
  EH-8 CI(metrics-privacy.yml)+ doctor + codex-guarded
✅ CLI 配線(5、apply 後) EH-4 / EH-5 bin/plangate verify —EH-4: V-1 前 strict / EH-5: V-1 後 warn(TASK-0143)
  EHS-1 bin/plangate verify —V-3 不合格時に validation_bias=strict で block(TASK-0145 増分1)
  EHS-3 bin/plangate verify —V-1 FAIL 時に fix-loop increment + validation_bias=strict で上限超過 block(TASK-0146 増分2)
  EHS-2 bin/plangate handoff --verify —handoff.md 6 要素不足を validation_bias=strict で block(TASK-0146 増分3)
⏳ doctor 可視化のみ(1) EH-7 bin/plangate doctor CLI Hook Wiring セクション + 手動推奨(#500 後続)

関連: responsibility-boundary.md / tool-policy.md / model-profiles.md 実装: scripts/hooks/check-plan-exists.sh / check-c3-approval.sh / check-plan-hash.sh / check-test-cases.sh / check-verification-evidence.sh / check-forbidden-files.sh / check-merge-approvals.sh / check-v3-review.sh / check-handoff-elements.sh / check-fix-loop.sh 設定例: .claude/settings.example.json / 単体テスト: tests/hooks/run-tests.sh

0. 運用モード別の強制実態(CLI 依存度 / 2026-06-28 現状把握)

各強制は「いつ発火するか」が経路ごとに異なる。とくに CLI(bin/plangate)を 通さない運用では、CLI 層の強制(EH-4 / EH-5 / EHS-1/2/3)は一度も発火しない

「ローカル強制」の前提に注意: 層 A(Claude PreToolUse)/ 層 E(Codex hooks)は Claude Code / Codex などの AI ツールを介して編集・コミットしたときのみローカル発火する。 エディタで直接ファイルを編集して git で直接コミットする完全手動運用では層 A/E も 発火しない(その場合の最終防壁は層 B の CI = PR/push トリガー)。以下の「手動 / AI 任せ」は 主に「AI ツールは使うが bin/plangate CLI は回さない」運用を指す。

発火層の分類

強制 発火契機 CLI を使わない運用での実態
A. Claude PreToolUse(自動・bypass 不能) EH-1 / EH-2 / EH-3 / EH-6 / EH-9 + EH-13(承認トークン直書き block / TASK-0123・TASK-1023)+ EH-12(apply 後) hook ごとに matcher が異なる(下記 §0.1)。EH-1 / EH-2 / EH-3 / EH-6 は Edit\|Write のみ、EH-9 / EH-12 は Bash のみ、EH-13 は両方 配線された matcher 経路でのみ常時発火(別経路は非発火 / #1104)
B. CI(自動・bypass 不能) EH-8(metrics privacy)/ settings drift / schema-validate / skip-ack / pr-issue-link PR / push ✅ 常時発火
C. CLIbin/plangate 実行時のみ) EH-4 / EH-5 / EHS-1 / EHS-2 / EHS-3 verify / handoff --verify実行したときだけ 🔴 休眠(CLI 未実行なら不発)
D. 外部設定 EH-7(マージ 2 段階レビュー) main へのマージ 🔶 GitHub branch protection 設定に依存(Human-owned admin)
E. Codex hooks EH-3 / check-script-basename Codex セッション中の apply_patch / Bash 等 (Claude Code 運用では非該当)

0.1 matcher 別の適用範囲(#1104 / 実測 2026-08-15・origin/main = dfaeebb

層 A(Claude PreToolUse)は「発火する / しない」ではなく どの tool 経路に配線されているかで 適用範囲が決まる。matcher に無い tool から同じ操作をしても hook は呼ばれない。 配線の正本は .claude/settings.jsonhooks 配列(HO 対象・Human-owned。本書は その写しであり、乖離した場合は settings.json が正)。

.claude/settings.json に配線済みの hook は 11 件。matcher 内訳は以下(実測)。 tracked な .claude/settings.example.json同一の matcher 集合(実測一致)であり、本ギャップは導入先にもそのまま配布される:

matcher event hook 守るもの
Edit\|Write PreToolUse check-plan-exists.sh(EH-1) plan.md 存在チェック
Edit\|Write PreToolUse check-c3-approval.sh(EH-2) C-3 承認ゲート
Edit\|Write PreToolUse check-plan-hash.sh(EH-3) Hardening Override 12 カテゴリ + plan.md ゲート + plan_hash 改竄
Edit\|Write PreToolUse check-forbidden-files.sh(EH-6) forbidden_files(scope 逸脱)
Edit\|Write PreToolUse check-approval-token-write.sh(EH-13) 承認トークン直書き
Bash PreToolUse check-approval-token-write.sh(EH-13) 承認トークン直書き(唯一の両経路配線
Bash PreToolUse check-plan-hash.sh(EH-3b / #1104・#1267) 現状は何も守らない(下記「#1267 の実測」)
Bash PreToolUse check-delegation-commit-boundary.sh(EH-9) 委譲 commit/push 境界
Bash PreToolUse check-git-destructive.sh(EH-12) protected branch 上の破壊的 git 操作
Edit\|Write\|MultiEdit PostToolUse scripts/hooks/check-post-edit-diff.sh 編集後 diff 可視化(block ではない)
(なし) Stop scripts/hooks/check-stop-diff-status.sh 停止時 diff 状態
(なし) SessionStart scripts/gh-pin-account.sh gh アカウント固定

明示: ファイル書き込みガードは Edit|Write 経路のみ

#1267 の実測(2026-08-28 / origin/main = 3f0cadd

PR #1267 が .claude/settings.example.jsonBash matcher へ check-plan-hash.sh を追加した (上表 EH-3b)。しかし配線だけでは何も守れない:

Bash 経路の欠落は #1104 で追跡中

含意

休眠ゲートを CLI 非依存で常時強制したい場合の選択肢(PR トリガーの CI 移植など)は 別途設計判断(新規 PBI / EPIC #527 の後続)とする。本節は現状把握であり方針は未確定。

1. 目的

PlanGate の Iron Law のうち runtime 強制可能な不変条件(現状 #1〜#7 相当)を、プロンプトに頼らず runtime で決定論的にブロック する。プロンプト薄型化(PBI-116-01 で達成)と両立して、強制力を維持する。なお Iron Law #8(出典照合)は決定論的 hook 化が困難なため、プロンプト + diff-audit(旧 self-review、ソフト面)で担保し runtime hook の対象外とする。

2. 強制すべき不変条件(一覧)

responsibility-boundary.md § 5 と整合。最低 6 件:

本節の「block」はすべて配線された matcher 経路での話(§0.1)。 EH-1 / EH-2 / EH-3 / EH-6 は Edit|Write のみ、EH-9 / EH-12 は Bash のみ、 EH-13 は両方に配線されている。未配線の経路からは同じ操作でも block されない(#1104)

EH-1: plan.md なし production code 編集ブロック

EH-2: C-3 承認なし exec ブロック

EH-3: plan_hash 改竄検知

Hardening Override(HO)12 カテゴリの block(Edit|Write 経路限定 / #1089 是正済み・9043536

EH-3 は plan_hash 検知に加え HO 12 カテゴリの block (正本: .claude/rules/mode-classification.md 承認境界周辺の変更節)を担う 唯一のガードである (check-forbidden-files.sh は HO パスを守らない)。

⚠️ 適用範囲は Edit|Write matcher に限定される(§0.1 / #1104)。EH-3 は .claude/settings.jsonEdit|Write 経路でのみ実効であるため(Bash 経路の配線は #1267 以降存在するが対象パスを解決できず一致しない / §0.1)、Bash tool 経由の 書き込み(cat > / tee / sed -i / python3 -c "open(...,'w')" 等)では HO も plan.md ゲートも発火しない。以下の「block される」はすべて Edit / Write 経路での話

Edit|Write 経路においては、HO 判定は task_id 分岐より前で行われるため、 TASK 文脈の有無に依らず block される (PR #1097 で是正。それ以前は PLANGATE_HOOK_TASK 設定時に 9 カテゴリすべてが 素通りしていた = #1089)。

残存脅威モデル(守るもの / 守らないもの)

  内容
守る Edit\|Write 経路の、字句上の表記揺れ(上記 7 変換クラスとその複合)による HO 迂回(#1101 適用後)
守らない Bash 経路(#1104)/ FS エイリアス・シンボリックリンク(上記 3・#1264。repo 外 symlink 経由の到達は上記 5・#1234)/ worktree 配下の HO パス(上記 4・#1277) / 監査ログ(hook-events.log)が書けない環境(log_eventset -eu 下で失敗し block(exit 2)に到達しない・#1278。rc は /bin/sh の実体依存で、bash 系は rc=1 = fail-open、dash・ash 系はリダイレクト失敗のみ rc=2=理由トークンなし。_audit のファイル化(mkdir -p 失敗)は dash でも rc=1。上記 6 の表を参照し「rc=2 だから守られている」と読まないこと) / hook を配線していない導入先(plugin 配布物に scripts/hooks/ は含まれない)/ PLANGATE_BYPASS_HOOK=1

EH-3 の HO block は多層防御の 1 層にすぎない。承認境界の最終的な保証主体は C-4 Human レビューGitHub branch protection であり、本 hook の block を 単独の保証と見なさないこと。

PLANGATE_HOOK_TASK 未設定セッションの正規経路(#1095)

EH-3 の no-task 経路は、コメント上「非 plan.md は SKIP」と読めるが、 実装は SKIP の前に PLANGATE_SKIP_REASON を必須とする(空なら exit 2)。 実際の挙動は次のとおり(判定順に評価される):

対象 条件 挙動
plan.md 有無を問わず block(TASK 文脈を消した plan 改変の阻止)
任意 メンテ窓が有効 MAINTENANCE_SKIPallowed_paths の範囲内。HO は除く)
非 HO の .md メンテ承認ファイル不在時のみ DOC_LIGHT_SKIP(自動 SKIP・skip-decision-log.jsonl に記録 / TASK-0138)
上記以外 PLANGATE_SKIP_REASON 未設定 blockSKIP 拒否: SKIP_REASON 未設定
上記以外 PLANGATE_SKIP_REASON 設定済み SKIP(skip-decision-log.jsonl へ記録・人間の追認が要る

doc-light はメンテ承認ファイルが存在しないときだけ発火する (失効済み・one_shot 消費済みのファイルが残っていても発火しない。 実装は doc-light 分岐をメンテ承認ファイル不在の条件で囲っている)。

したがって no-task セッションで編集する正規経路は 3 つ:

# 経路 副作用
A PLANGATE_HOOK_TASK=TASK-XXXX起動時に設定 HO は #1089 是正済みのため保護は維持される
B PLANGATE_SKIP_REASON="..."起動時に設定 skip が記録され acknowledged_by の人間追認が要る(CI が未追認を fail)
C メンテ承認ファイルを人間が発行maintenance-cli.md 窓つき / one_shot。AI は発行できない

A / B は起動時固定の env であり、実行中のセッションからは変更できない。 C はディスク上の承認ファイルを hook が起動ごとに読むため、セッション再起動を要しない (正本: maintenance-cli.md)。

EH-4: test-cases.md なし V-1 ブロック

EH-5: 検証ログなし PR 作成ブロック

EH-6: scope 外ファイル編集検知

EH-7: 2 段階レビューなしマージブロック(推奨)

EH-9: 委譲 commit/push 境界検知(F2 / TASK-0073)

EH-12: protected branch 上の破壊的 git 操作ブロック

3. validation_bias: strict 時の追加条件(EHS)

設計ステータス: 配線・適用済み(TASK-0145 / 0146 / 0147)。スクリプト実装に 加え、発火条件 validation_bias: strictbin/plangate に配線済み。発火条件の供給は --profile <key>model-profiles.yamlvalidation_bias 解決 → PLANGATE_VALIDATION_BIAS 内部 export(TASK-0147 / #644)。 CLI 依存度の注意: これらは CLI 層(§0 の層 C)であり、bin/plangate verify / handoff --verify を実行したときのみ発火する(手動 / AI 任せ運用では休眠。CI 移植は TASK-0148 で検討)。

Model Profile の validation_bias: strict プロファイル(gpt-5_5_pro 等)では、 上記 EH-1〜EH-7 に加えて以下 3 件を追加で強制:

EHS-1: V-3 外部レビュー必須化

EHS-2: handoff.md 必須 6 要素チェック

EHS-3: V-1 fix loop 上限超過 escalation

EHS 発火条件の供給(配線済み)

validation_bias: strictdocs/ai/model-profiles.yaml で定義され、実行時の供給は 配線済み(TASK-0147 / #644): bin/plangate verify / handoff --verify--profile=<key> (等号形式)を受理し、model-profiles.yamlvalidation_biasscripts/_resolve_validation_bias.py で解決して PLANGATE_VALIDATION_BIAS を内部 export する。env で明示注入済みなら尊重し、 normal/lenient・無指定では非発火(既存挙動不変)。未知 key / yaml 欠落時は normal fallback + stderr 警告。

残課題: 上記は CLI 層(§0 層 C)のため、CLI を回さない運用では休眠する。PR トリガーの CI へ EHS-1 / EHS-2 を移植して CLI 非依存で常時強制する案は TASK-0148 で検討(EHS-3 は CLI プロセス計数のため CLI 維持)。

4. 実装(#157 で 3 hook + #169 で 7 hook = 計 10 hook、すべて完了)

Hook 種別 実装 由来
EH-1(plan.md なし production code 編集 block) PreToolUse hook scripts/hooks/check-plan-exists.sh #169 セッション A / TASK-0056
EH-2(C-3 未承認 exec block) PreToolUse hook scripts/hooks/check-c3-approval.sh #157 / TASK-0048
EH-3(plan_hash 改竄検知) PreToolUse hook + CLI scripts/hooks/check-plan-hash.sh #169 セッション A / TASK-0056
EH-4(test-cases.md なし V-1 block) CLI(V-1 前で呼び出し) scripts/hooks/check-test-cases.sh #169 セッション B / TASK-0057
EH-5(検証ログなし PR 作成 block) CLI(PR 作成前で呼び出し) scripts/hooks/check-verification-evidence.sh #169 セッション B / TASK-0057
EH-6(scope 外ファイル編集検知) PreToolUse hook + CLI scripts/hooks/check-forbidden-files.sh #169 セッション B / TASK-0057
EH-7(2 段階レビューなしマージ block) CLI(マージ前で呼び出し) scripts/hooks/check-merge-approvals.sh #169 セッション C / TASK-0058
EHS-1(V-3 外部レビュー必須化) CLI(mode 連携) scripts/hooks/check-v3-review.sh #169 セッション C / TASK-0058
EHS-2(handoff 必須 6 要素) CLI(手動 / WF-05 で呼び出し) scripts/hooks/check-handoff-elements.sh #157 / TASK-0048
EHS-3(fix loop 上限超過) CLI(V-1 fix loop 内で increment / check) scripts/hooks/check-fix-loop.sh #157 / TASK-0048
EH-9(委譲 commit/push 境界検知) PreToolUse hook(Bash 前) scripts/hooks/check-delegation-commit-boundary.sh #239 問題2 / TASK-0073
auth-preflight(exec 前 認証三点検証) CLI(exec 前で呼び出し) scripts/hooks/check-auth-preflight.sh #239 問題3 / TASK-0073
EH-12(protected branch 上の破壊的 git 操作 block) PreToolUse hook(Bash 前・Human apply 後に有効 scripts/check-git-destructive.sh(HO 外・単一ソース)+ 配線 scripts/apply-eh-git-destructive-guard.sh 2026-08-02 main 上 reset --hard 実害

残未実装: なし(10/10 完了)。EH-7 の GitHub branch protection 自動連携は別 PBI 候補。

4.1 3 モード設計

モード 環境変数 挙動
default(推奨初期値) なし 違反検出時は warning のみ、continue:true(block しない)。誤検出時の作業妨害を最小化
strict PLANGATE_HOOK_STRICT=1 違反検出時に block / exit 1。本番運用 / CI 等で有効化
bypass PLANGATE_BYPASS_HOOK=1 常時 pass。緊急対応 / 既知の例外時のみ使用、監査 log に必ず記録

4.2 監査ログ

すべての判定は docs/working/_audit/hook-events.log に append-only で記録される(タブ区切り):

<ISO8601 UTC>\t<level>\t<hook-name>\t<task-id>\t<message>

level: PASS / VIOLATION / BYPASS / SKIP / INCREMENT

4.3 設定方法(opt-in)

.claude/settings.example.json.claude/settings.json にコピーすると PreToolUse hook(EH-1 + EH-2 + EH-6matcher = Edit|WriteEH-3Edit|Write + Bash〔EH-3b / #1104。no-task では BASH_LANE_NOOP で何も守らない〕)+ SessionStart(gh-pin-account)が有効化される。Bash 経路の書き込みは EH-3b でも解析されない(§0.1 / #1104 open)。

apply を実行する Human が、settings を apply する前に git status --porcelain が空であることを確認したうえで 共有 checkout を patched hook を含む ref(main)へ切り替える(未コミット変更がある共有 checkout を AI が切り替えない)。settings だけ先に apply すると旧 hook + Bash 配線で no-task の全 Bash が停止する(2026-09-05 実害)。apply 後は grep -c BASH_LANE_NOOP scripts/hooks/check-plan-hash.sh が 1 以上であることを確認する(apply 手順の正本は settings-wiring-contract.md「検証・適用」)。

CLI 配線(TASK-0143 / apply-script 適用後): EH-4 は plangate verify V-1 前(strict=1)、EH-5 は V-1 後(warn)で発火。EH-7 / EHS-1 / EHS-2 / EHS-3 は引き続き手動呼び出し。

4.4 テスト

4.5 全 10 hook 完了(#169 完走)

Issue #169 完了。10/10 hooks 実装済(CLI / PreToolUse 構成)。残課題は GitHub branch protection 自動連携(EH-7 の上位拡張、外部 GitHub API 操作を伴うため別 PBI)。

5. 既存 .claude/settings.json hooks との関係

本ファイルが定義する不変条件は、既存 hooks(もしあれば)と:

6. 「block 通知」の文言ガイド

Hook が block する際の通知文言:

[Hook EH-1] plan.md がないため production code を編集できません。
docs/working/TASK-XXXX/plan.md を作成してください。

形式:

関連