配布資料の目次 › 設定カタログ
配布資料 2606 — フックの実物(このリポジトリで動いているソース全文)
このファイルは
tools/build_hooks_doc.pyが.claude/hooks/*.shと.claude/settings.jsonから生成する。コードを手で貼り直さない(貼り直すと必ず腐る)。
フックは特別な技術ではありません。条件を判定して、満たさなければエラーを出して終わる。 それだけです。
とはいえ「それだけです」と言われても、実物を見ないと書ける気がしないと思います。 そこで、この教材リポジトリ自身を動かしているフック全文をそのまま載せます。
書いたのは AI です。人間が出したのは、原則と、事故の認定と、承認だけでした。 コメントに事故の経緯が残っているので、そこも含めて読んでみてください。 どのフックも、先に事故があってから生まれています。
何を止めているか
| フック | イベント | 何を止めるか | 生まれたきっかけ |
|---|---|---|---|
session-start-rules.sh |
SessionStart | (止めない)規律バナーを注入し、前セッションのモードを持ち越さない | 確認モードが翌セッションへ持ち越されて、原因の見えない編集禁止になった |
user-prompt-task-mode.sh |
UserPromptSubmit | (止めない)「確認して」と「実行して」を判定して記録する | 調査のつもりが勝手に直された |
pre-edit-task-mode-guard.sh |
PreToolUse(Edit|Write) | 確認モード中のリポジトリ内編集 | 同上(判定と執行を分けた) |
pre-edit-generated-guard.sh |
PreToolUse(Edit|Write) | 生成物ディレクトリへの直接編集 | 生成物を手で直して、次の再生成で消えた |
pre-commit-regen-check.sh |
PreToolUse(Bash) | 素材だけ変えて生成物を伴わないコミット | スライドを直して A4 資料を作り直し忘れた |
判定(プロンプトを読む)と執行(ツールを止める)が別のフックに分かれているのが要点です。 それぞれを単純に保てますし、片方が壊れてももう片方は生きています。
読むときの勘所
exit 2が「止める」の合図です。exit 0は通します- fail-open。 入力が読めない、
python3が無いといったフック側の不調では必ず通します。 フックの故障で正当な作業を止めないためです - バイパスは痕跡が残る形にします。 環境変数を黙って渡せる形にすると、 「なぜ通したのか」が後から分かりません
- stdin は判定より先に読み切ります。 読まずに exit すると呼び出し側が EPIPE を踏んで、 素通りさせたいだけの場面でエラー扱いの雑音が出ます
- 止め方は公式に明記された手段を使います。 契約から外れた形は、将来壊れるときに 「黙って通る」方向に壊れます
最後の1点は実際に踏みました。詳しくは pre-edit-task-mode-guard.sh の冒頭コメントにあります。
配線(settings.json)
フック本体はただのスクリプトで、いつ呼ぶかはここが決めます。
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/session-start-rules.sh\""
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/user-prompt-task-mode.sh\""
}
]
}
],
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/pre-edit-task-mode-guard.sh\""
},
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/pre-edit-generated-guard.sh\""
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/pre-commit-regen-check.sh\""
}
]
}
]
}
}
matcher でツールを絞り、Bash 系はさらにスクリプト内で自己判定する二重ゲートにしてあります。
配線を書き換えたときに、フックが黙って全コマンドで走り出す事故を避けるためです。
session-start-rules.sh
セッションの冒頭で規律バナーを流すだけの、いちばん単純なフックです。止めません。
ひとつだけ仕事をしています。前のセッションのタスクモードを持ち越さないことです。
.claude/task-mode はディスクに残ります。前のセッションが確認モードで終わると、
次のセッションが黙って編集禁止で始まります。最初のプロンプトが判定に当たらない限り
解除されないので、原因が見えないまま編集が全部ブロックされます。実際に踏みました。
startup と clear では消し、resume と compact では残します。同じ作業の続きだからです。
#!/bin/bash
# SessionStart: 規律バナーを注入し、前セッションのタスクモードを持ち越さない。
INPUT=$(cat)
DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
# .claude/task-mode はディスクに残るため、前のセッションが確認モードで終わると
# 次のセッションが**黙って編集禁止で始まる**。最初のプロンプトが判定に当たらない
# 限り解除されないので、原因が見えないまま編集が全部ブロックされる。
# 新規セッション(startup / clear)ではモードを持ち越さない。
# resume / compact は同じ作業の続きなので保持する。
SRC=$(printf '%s' "$INPUT" | python3 -c \
'import json,sys; print(json.load(sys.stdin).get("source",""))' 2>/dev/null) || SRC=""
case "$SRC" in
startup|clear) rm -f "$DIR/.claude/task-mode" ;;
esac
# 規律バナー(最初のツール呼び出し前から文脈に在中させるのが目的・常に exit 0)
cat <<'EOF'
[教材リポジトリ 規律バナー]
1. 生成物(slides/handouts・workbook・sample・pdf / dist)は手で編集しない。
元ファイルを直して再生成する(対応表は CLAUDE.md)
2. スライド(slides/dayN.html)を直したら、A4資料も必ず作り直す
3. 配布物は許可リスト方式。tools/handouts_manifest.tsv に載せたものだけが出る。
除外リスト方式に変えない。提供側専用の文書を載せない(一覧は CLAUDE.md)
4. 一括置換で史実を壊さない。期間・数値を含む文は1件ずつ文脈を見る
5. PDF は tools/verify_pdf.py で実測してから完了と言う。見た目の印象で判断しない
6. タスクモード: 「確認して」=調査・計画のみ(編集禁止) /「実行して」=配布物まで通す
EOF
exit 0
user-prompt-task-mode.sh
「確認して」と「実行して」を判定して、.claude/task-mode に書くだけのフックです。
執行は別のフックが担当します。
見どころは3層の順序です。
最初にこれを書いたとき、承認だけの短い返事——「はい、お願いします」「それでいいのでやって」——を 拾えず、計画を承認したのに何も編集できないデッドロックになりました。 しかも編集ガードのほうが「自己判断での切替は禁止」と言うので、AI 側からは解除できません。
そこで層を分けました。作業指示 → 確認指示 → 相槌、の順で見ます。 「はい、まず確認して」が確認モードになるように、相槌をいちばん後ろに置くのが要点です。
正規表現に「どうやって〜?」を除外する (^|[^う])やって が入っているのも、
実際に質問を作業指示と誤判定したからです。
#!/bin/bash
# UserPromptSubmit: タスクモードを判定して .claude/task-mode に永続化する。
#
# 3層で見る。承認だけの短い返事(「はい、お願いします」「それでいいのでやって」)を
# 拾えないと、**計画を承認したのに何も編集できないデッドロック**になる。
# しかも編集ガードは「自己判断での切替は禁止」と言うので、Claude 側からは解除できない。
# 実際に「はい」「OK」「お願いします」「続けて」がすべて確認モードのままだった。
#
# 1. 明示的な作業指示 → execute(「確認して」が混ざっていても作業指示が勝つ)
# 2. 明示的な確認指示 → investigate
# 3. 承認・同意だけの返事 → execute(直前に出した計画への GO とみなす)
# 4. どれにも当たらない → 現在のモードを変えない
#
# 3 を 2 より後ろに置くのが要点。「はい、まず確認して」は確認モードになる。
INPUT=$(cat)
DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
MODE="$DIR/.claude/task-mode"
# 通知系のプロンプトはユーザー指示ではないので判定に使わない
if printf '%s' "$INPUT" | grep -q 'task-notification'; then exit 0; fi
# プロンプト本文だけを見る(JSON のメタ情報に含まれる語で誤判定しないため)
P=$(printf '%s' "$INPUT" | python3 -c \
'import json,sys; print(json.load(sys.stdin).get("prompt",""))' 2>/dev/null) || P=""
[ -n "$P" ] || P="$INPUT"
# 第1層には「動け」と言っている語だけを置く。CLAUDE.md の
# 「実装を確認して進めて」のような混在は作業指示とみなす、を守るため
# 進めて・やって・やろう・頼む はここ(確認語が混ざっても作業指示が勝つ)。
# 「どうやって〜?」は質問なので (^|[^う])やって で除外する。
EXEC='実行して|作業して|実装して|修正して|直して|なおして|生成して|作り直して|作って|デプロイして|反映して|更新して|追加して|削除して|対応して|適用して|コミットして|やり直して|進めて|(^|[^う])やって|やろう|頼む'
INVES='確認して|調査して|チェックして|レビューして|監査して|計画(を|して)|調べて|教えて|どうなって|見て(ください|くれ|ほしい)'
# 第3層は相槌だけ。「お願いします。先に調べて」は確認指示が勝ってほしいので
# お願い はここに置く(第1層に上げると確認依頼まで execute になる)。
APPROVE='^(はい|うん|了解|承知|オーケー|いいよ|よろしく)|^(ok|OK|Ok|go|GO|Go)([^A-Za-z]|$)|お願い|続けて|それで(いい|ok|OK)'
if printf '%s' "$P" | grep -Eq "$EXEC"; then echo execute > "$MODE"
elif printf '%s' "$P" | grep -Eq "$INVES"; then echo investigate > "$MODE"
elif printf '%s' "$P" | grep -Eq "$APPROVE"; then echo execute > "$MODE"
fi
if [ "$(cat "$MODE" 2>/dev/null)" = "investigate" ]; then
echo '[task-mode] 現在は確認モード(調査・計画のみ)。ファイル編集・コミットは禁止。編集開始はユーザーの明示指示を待つこと。'
fi
exit 0
pre-edit-task-mode-guard.sh
確認モード中の編集を実際に止めるほうです。判定は前のフックがやっています。
冒頭のコメントに、止め方を公式な手段に揃えた経緯が残っています。
{"decision":"block"} を stdout に出す形は今も効きますが、PreToolUse の公式な停止手段では
ありません。契約から外れた形は、将来壊れるときに「黙って通る」方向に壊れます。
リポジトリの外への書き出しは通します。調査の途中でメモを書くのは調査の一部だからです。
#!/bin/bash
# PreToolUse(Edit|Write): 確認モード中のリポジトリ内ファイル編集をブロックする。
# hook 自身の障害では止めない(fail-open)。
#
# 停止は exit 2 + stderr で行う。stdout の {"decision":"block"} は今も効くが、
# PreToolUse の公式な停止手段ではない(正式は hookSpecificOutput.permissionDecision)。
# 契約から外れた形は、将来壊れるときに「黙って通る」方向に壊れる。
# exit 2 は PreToolUse の停止手段として明記されているので、3つの hook をこれに揃える。
DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
# stdin は判定より先に読み切る。読まずに exit すると呼び出し側が
# EPIPE を踏んで、素通りさせたいだけの場面でエラー扱いの雑音が出る
INPUT=$(cat)
[ "$(cat "$DIR/.claude/task-mode" 2>/dev/null)" = "investigate" ] || exit 0
FILE=$(printf '%s' "$INPUT" | python3 -c \
'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("file_path",""))' \
2>/dev/null) || FILE=""
[ -n "$FILE" ] || FILE=$(printf '%s' "$INPUT" \
| sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
# リポジトリ外(スクラッチ等)への書き出しは調査の一部として許す
case "$FILE" in
"$DIR"/*) ;;
*) exit 0 ;;
esac
cat >&2 <<'EOF'
[task-mode] 確認モード中(調査・計画のみ)のため、リポジトリ内ファイルの編集を
ブロックしました。調査結果の報告と作業計画の提示に留めてください。
編集に入るのはユーザーが判断して指示すること。自己判断で .claude/task-mode を
書き換えて回避してはいけない。
EOF
exit 2
pre-edit-generated-guard.sh
生成物ディレクトリへの直接編集を止めます。
生成物を手で直しても、次の再生成で必ず消えます。しかも「直したつもり」で配布物に反映 されないまま出ていくのが最悪の型なので、書けないようにしました。
このフックは止めるだけでなく、どこを直せばいいかを教えます。 パスから逆引きして 「元ファイルはこれ、再生成コマンドはこれ」と出します。 止めるフックは、止めた先の行き先まで示さないと、結局バイパスされます。
#!/bin/bash
# PreToolUse(Edit|Write): 生成物ディレクトリへの直接編集をブロックする。
#
# 生成物を手で直しても、次の再生成で必ず消える。しかも「直したつもり」で
# 配布物に反映されないまま出ていくのが最悪の型なので、書けないようにする。
# 正当に触る必要がある場合(生成器の実験など)は理由を添えて
# GENERATED_EDIT_OK=1
# を環境変数ではなくユーザーの明示指示として受け、この hook を一時的に外す。
#
# 停止は exit 2 + stderr。stdout の {"decision":"block"} は PreToolUse の公式な
# 停止手段ではなく、将来「黙って通る」方向に壊れうる。あわせて、理由文を JSON に
# 組み立てるのをやめたので、パスに引用符が混ざっても壊れなくなった。
DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
# stdin は判定より先に読み切る(読まずに exit すると呼び出し側が EPIPE を踏む)
INPUT=$(cat)
[ -n "$GENERATED_EDIT_OK" ] && exit 0
FILE=$(printf '%s' "$INPUT" | python3 -c \
'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("file_path",""))' \
2>/dev/null) || FILE=""
[ -n "$FILE" ] || FILE=$(printf '%s' "$INPUT" \
| sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
[ -n "$FILE" ] || exit 0
# リポジトリからの相対パスに直す
REL="${FILE#$DIR/}"
case "$REL" in
slides/handouts/*|slides/workbook/*|slides/sample/*|slides/pdf/*|dist/*) ;;
*) exit 0 ;;
esac
case "$REL" in
slides/handouts/*) SRC="素材(catalog/ chronicle/ incidents/ handbook/ curriculum/ docs/ keihi/)と tools/handouts_manifest.tsv"; CMD="python3 tools/build_handouts.py" ;;
slides/workbook/*) SRC="slides/script/解説_dayN.md と slides/dayN.html"; CMD="python3 tools/build_workbook.py" ;;
slides/sample/*) SRC="tools/build_sample.py の PICKS と、抜粋元のスライド・解説"; CMD="python3 tools/build_sample.py" ;;
slides/pdf/*) SRC="slides/dayN.html"; CMD="bash build-dist.sh(または Chrome の --print-to-pdf)" ;;
dist/*) SRC="各素材"; CMD="bash build-dist.sh" ;;
esac
cat >&2 <<EOF
[generated-guard] $REL は生成物です。手で直しても次の再生成で消えます。
元ファイル: $SRC
再生成 : $CMD
生成器そのものを試すなど、正当に生成物へ直接書く必要がある場合は、
その理由をユーザーに提示して承認を得てください。
EOF
exit 2
pre-commit-regen-check.sh
このリポジトリでいちばん長いフックです。長い理由が全部コメントに書いてあります。
3回作り直しています。
1回目は mtime で新旧を比べていました。checkout や touch で簡単に狂うのと、同一秒に
丸められて見逃すので、同じコミットに生成物が入っているかで見る方式に変えました。
2回目は入力 JSON 全体に部分一致させていて、grep -rn "git commit" .claude のような
調査コマンドまで止めました。誤検知は検知漏れと同じくらい害があります。
毎回鳴るフックは、必ず無視されるようになるからです。
3回目は core.quotepath です。既定では非 ASCII パスが8進エスケープされて出るので、
日本語ファイル名が正規表現に一生当たらず、検知器が黙って死んでいました。
git add -A && git commit のように stage とコミットを1コマンドにまとめられると、
このフックの時点では stage が空に見えて検査できません。そこは素通りさせずに止めています
(fail-closed)。素通りさせると、チェックが丸ごと無効になるからです。
末尾には、別リポジトリへ生成した公開サイトが古くないかを一時ディレクトリへ作り直して 差分で見る判定も入っています。ここも mtime では判定していません。
#!/bin/bash
# PreToolUse(Bash): git commit を検知し、素材だけ変えて生成物を伴っていないコミットを止める。
#
# 防ぎたい事故: 「スライドを直したが A4 資料を作り直していない」
# 「マニフェストに足したが配布資料を生成していない」
# → 配布物と素材が食い違ったまま出ていく。
#
# 判定は mtime ではなく **同じコミットに生成物が入っているか** で行う。
# mtime は checkout や touch で簡単に狂うのと、同一秒に丸められて見逃すため
# (最初の実装は mtime 比較で、導通テストで実際に検知できなかった)。
# 再生成しても差分が出ない(コメントだけの変更など)ときは、その旨を報告した上で:
# REGEN_OK=1 git commit ...
INPUT=$(cat)
# 判定は入力 JSON 全体ではなく tool_input.command に対して行う。
# 全体への部分文字列一致だと `grep -rn "git commit" .claude` のように
# コマンド名を含むだけの調査まで止めた(実測でブロックされた)。
# 毎回鳴るフックは必ず無視されるようになるので、誤検知は検知漏れと同じくらい害がある。
CMD=$(printf '%s' "$INPUT" | python3 -c \
'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))' \
2>/dev/null) || CMD=""
# 取り出せなければ入力全体で見る(誤検知するほうが、素通りさせるよりまし)
[ -n "$CMD" ] || CMD="$INPUT"
# コマンド位置=行頭か ; && || の直後にある git commit だけを見る。
# 先頭の環境変数代入(REGEN_OK=1 git commit …)は透過させる。
# 既知の割り切り: bash -c 'git commit' のようにクォートの内側に隠れた形は拾わない。
# 実際に書く形(git commit / … && git commit)を確実に拾うほうを優先している。
printf '%s' "$CMD" | grep -Eq \
'(^|[;&|])[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*git[[:space:]]+commit([[:space:]]|$)' \
|| exit 0
printf '%s' "$CMD" | grep -q 'REGEN_OK=1' && exit 0
DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
# このフックはこのリポジトリの生成物だけを見る。
# コマンドの先頭が別リポジトリへの `cd` なら、コミット先はそちらなので黙って通す。
# ここを見ないと、隣の homepage リポジトリへのコミットまで「stage が空」で止める
# (実際に止めた。フックは自分の管轄外まで殺しにいってはいけない)。
TARGET=$(printf '%s' "$CMD" | sed -nE 's|^[[:space:]]*cd[[:space:]]+([^&;|]+).*|\1|p' \
| head -1 | sed -E 's/[[:space:]]+$//' | tr -d "'\"")
if [ -n "$TARGET" ]; then
HERE=$(cd "$DIR" 2>/dev/null && git rev-parse --show-toplevel 2>/dev/null) || HERE=""
THERE=$(cd "$TARGET" 2>/dev/null && git rev-parse --show-toplevel 2>/dev/null) || THERE=""
if [ -n "$THERE" ] && [ "$THERE" != "$HERE" ]; then exit 0; fi
fi
cd "$DIR" 2>/dev/null || exit 0
# core.quotepath は既定 true で、非 ASCII パスを
# "slides/script/\350\247\243\350\252\254_day1.md"
# とエスケープして出す。日本語ファイル名(解説_dayN.md)が下の正規表現に一生
# 当たらず、検知器が黙って死ぬ。実際、解説文だけを直したコミットは素通りしていた。
STAGED=$(git -c core.quotepath=false diff --cached --name-only 2>/dev/null) || exit 0
# このフックはコミット実行「前」に走る。`git add -A && git commit -m x` や
# `git commit -am x` のように stage とコミットを1コマンドにまとめられると、
# ここでは stage が空に見え、素材と生成物の食い違いを検査できない。
# 素通りさせるとチェックが丸ごと無効になるので、先に stage させる(fail-closed)。
if [ -z "$STAGED" ]; then
# メッセージだけ直す amend は stage が空で正常なので通す
printf '%s' "$CMD" | grep -q -- '--amend' && exit 0
cat >&2 <<'EOF'
[pre-commit-regen-check] stage が空のままコミットしようとしています。
このフックはコミット実行前に走るため、add とコミットを1コマンドにまとめると
素材と生成物の食い違いを検査できません。2回に分けてください。
git add <ファイル…> ← 先にこれだけ実行する
git commit -m "…" ← 次にこれ
意図して素通りさせる場合は、その理由を報告した上で:
REGEN_OK=1 git commit …
EOF
exit 2
fi
has() { printf '%s' "$STAGED" | grep -qE "$1"; }
MISSING=""
need() { MISSING="$MISSING
- $1"; }
# スライド / 解説 / 生成器 を変えた → A4 資料が同じコミットに要る
if has '^slides/day[0-9]+\.html$|^slides/script/解説_|^tools/build_workbook\.py$' \
&& ! has '^slides/workbook/'; then
need "A4資料が入っていません → python3 tools/build_workbook.py"
fi
# 素材 / マニフェスト / 生成器 を変えた → 配布資料が同じコミットに要る
if has '^(catalog|chronicle|incidents|handbook|curriculum|docs|keihi)/|^tools/handouts_manifest\.tsv$|^tools/build_handouts\.py$' \
&& ! has '^slides/handouts/'; then
need "配布資料が入っていません → python3 tools/build_handouts.py"
fi
# フックそのものを変えた → その全文を載せた配布資料も作り直す
if has '^\.claude/(hooks/|settings\.json$)|^tools/build_hooks_doc\.py$' \
&& ! has '^catalog/06_hooks_source\.md$'; then
need "フックの全文資料が古いままです → python3 tools/build_hooks_doc.py"
fi
# 抜粋の選定を変えた → サンプルが同じコミットに要る
# 抜粋元のスライドは条件に入れない。サンプルは7ページしか使わないので、
# 大半のスライド修正では正当に差分が出ない。毎回鳴る hook は必ず無視されるようになる
# (配布時は build-dist.sh が全部作り直すので、配布物の整合はそちらで担保される)
if has '^tools/build_sample\.py$' && ! has '^slides/sample/'; then
need "案内用サンプルが入っていません → python3 tools/build_sample.py"
fi
# 配色・版面を変えた → スライドを埋め込む生成物に及ぶ
# 対象は A4資料とサンプル(と公開サイト。こちらは後段の差分判定で見る)。
# 配布資料は slides/design を読まない——build_handouts.py が自前の CSS を持っており、
# 単体で開ける HTML にするため値を転記している。handouts を条件に入れていたせいで、
# 版面だけ直したコミットが「配布資料が足りない」と誤検知されて止まった。
# 誤検知は検知漏れと同じくらい害がある(毎回鳴る hook は必ず無視されるようになる)。
if has '^slides/design/.*\.css$' && ! ( has '^slides/workbook/' && has '^slides/sample/' ); then
need "版面の変更は A4資料とサンプルに及びます → python3 tools/build_workbook.py && python3 tools/build_sample.py"
fi
# 公開サイトを変えた → homepage リポジトリの生成物が古くないか確かめる。
# 出力先が別リポジトリなので「同じコミットに入っているか」では判定できない。
# 代わりに一時ディレクトリへ作り直して差分を取る(mtime では判定しない)。
# sitemap.xml は lastmod に当日日付を書くので、日をまたぐだけで差分になる。除外する。
SITE="../homepage/public/course/ai-driven"
if has '^(catalog|chronicle|incidents|handbook|curriculum|docs|keihi|site)/|^slides/day[0-9]+\.html$|^slides/script/解説_|^slides/design/.*\.css$|^tools/(build_web|build_handouts|build_workbook)\.py$|^tools/handouts_manifest\.tsv$' \
&& [ -d "$SITE" ]; then
TMP=$(mktemp -d) || TMP=""
if [ -n "$TMP" ]; then
if python3 tools/build_web.py "$TMP" >/dev/null 2>&1 \
&& ! diff -rq --exclude=sitemap.xml "$TMP" "$SITE" >/dev/null 2>&1; then
need "公開サイトが古いままです → bash build-site.sh(出力先は $SITE)"
fi
rm -rf "$TMP"
fi
fi
[ -n "$MISSING" ] || exit 0
cat >&2 <<EOF
[pre-commit-regen-check] 素材を変更しましたが、対応する生成物が同じコミットに入っていません。
$MISSING
再生成して、生成物も一緒に stage してからコミットしてください。
再生成しても差分が出ない場合(コメントだけの変更など)は、その旨を報告した上で:
REGEN_OK=1 git commit ...
EOF
exit 2
持ち帰るなら
いきなり5本を真似する必要はありません。自分のプロジェクトで実際に踏んだ事故から 1本ずつにしてください。踏んでいない地雷の対策は、たいてい形骸化します。
AI に書かせるときは、依頼に3つだけ入れれば足ります。
- 検知する条件(どのコマンド・どのファイル・どの状態か)
- 止めるのか、警告するのか
- バイパスのトークン名と、理由の記録先
受け取ったら、フェイルオープンがどこに入ったかを指させてください。 そこを説明できないスクリプトは、まだ本番に配線しないほうがいいです。
そして最後に一つ。わざと壊して、本当に鳴るかを確かめてください。 「設定した」と「鳴る」は別物です。ここにある検知器も、それで3回作り直しています。