Claude Code Hooks セットアップ
CLAUDE.md の禁止事項を LLM 判断ではなくシェルレベルで強制するためのフック設定。
permissions.deny(静的パターン)と PreToolUse フック(動的検査)の二段構え。
なぜ Hook 化するのか
CLAUDE.md は system-reminder で「関連性なければ無視せよ」と明示注入されるため、**強制力のない助言(advisory)**に過ぎない。
不可逆な被害を伴う禁止(main 直接 push、機密ファイル混入、本番デプロイ事故)は、シェルレベルで止めない限り事故を完全には防げない。
- CLAUDE.md = behavioral guidance(LLM が判断、確率的)
- Hooks /
permissions.deny= technical enforcement(ハーネスが判断、決定論的)
構成
| ファイル | 役割 |
|---|---|
scripts/hooks/pre_bash_guard.sh | PreToolUse Bash フック本体。動的パターン検査 |
scripts/hooks/post_edit_guard.sh | PostToolUse Edit/Write フック本体。コード違反検知 |
scripts/hooks/instructions_loaded_log.sh | InstructionsLoaded フック本体。CLAUDE.md / rules のロード記録(下記参照) |
scripts/hooks/session_start_prompts.sh | SessionStart フック本体。運用プロンプト受信箱の自動注入(下記参照) |
scripts/hooks/context_budget_warn.sh | Stop フック本体。context 肥大時に handover / 新セッションを促す警告(下記参照) |
scripts/hooks/settings.example.json | .claude/settings.json の雛形(permissions.deny + PreToolUse / PostToolUse / InstructionsLoaded / SessionStart / Stop 配線) |
.claude/settings.json | 各ワークスペースの実設定(gitignore・各自で配置) |
scripts/pre-push-check.sh | git pre-push hook(既存・本フックと相補) |
初回セットアップ(各ワークスペースで実施)
テンプレートを
.claude/settings.jsonにコピー(.claude/は gitignore)mkdir -p .claude cp scripts/hooks/settings.example.json .claude/settings.json実行権限を確認(
git clone直後に落ちることがある)chmod +x scripts/hooks/pre_bash_guard.shClaude Code を再起動して設定読み込みを反映
動作確認: 安全なコマンドを試す → 通る、危険コマンドを試す → 拒否される
ユーザ: 「git push origin main を実行して」 → Claude が Bash 呼出 → Hook が exit 2 → ❌ Blocked: ... が表示される
何がブロックされるか
permissions.deny(静的・即時拒否)
| パターン | 理由 |
|---|---|
Bash(git push origin main*) | main への直接 push 禁止(PR 経由のみ) |
Bash(git push origin master*) | (リネーム未対応リポ向けの保険) |
Edit/Write(./.clasprc.json) | clasp 認証トークン |
Edit/Write(./creds.json) | GCP サービスアカウント |
Edit/Write(./.clasp.json) | clasp 作業コピー(dev/prod 切替で生成) |
Edit/Write(./.env) | 環境変数(API キー等) |
PreToolUse Bash フック(動的・スクリプト判定)
| パターン | 検知例 | 例外 |
|---|---|---|
rm -rf | rm -rf ./important | node_modules / dist / build / coverage / .next / .cache / tmp は許可 |
git reset --hard | git reset --hard HEAD~3 | なし(巻戻しは手動で) |
git push --force / -f / --force-with-lease | git push -f origin feature | なし |
clasp push 直接呼出 | clasp push -f | npm run push:dev 経由を強制 |
npm run push:prod / deploy:prod | 同左 | なし(CI 専属、手動は禁止) |
機密ファイルの git add/commit | git add .clasprc.json / git commit -m wip .env | .clasp.dev.json .clasp.prod.json は committed テンプレートなので除外 |
.claude/settings.json のシェル経由書換 | echo {} > .claude/settings.json | Edit ツール経由は許可(明示的承認のため) |
PostToolUse Edit/Write/MultiEdit フック(コード書込み後検知)
Edit / Write / MultiEdit の直後に走り、Claude が新たに書込んだコード片にコーディング規約違反があれば exit 2 で通知する。
既存ファイルに残存する違反は誤検知しない — 旧コンテンツ (Edit の old_string / Write の git HEAD 内容) と新コンテンツの出現回数を比較し、新規が増えた場合のみ通知する。
| パターン | 検知例 | 例外 |
|---|---|---|
PropertiesService.getScriptProperties() 直呼び | var p = PropertiesService.getScriptProperties(); | mas/000_infra/001_env.js(Env モジュール本体) |
| prod scriptId のハードコード | var s = '12X9...';(.clasp.prod.json の値と一致) | .clasp.*.json テンプレ自身、001_env.js |
CLAUDE.md / CLAUDE.local.md の @ import | @docs/_internal/foo.md / @CLAUDE.local.md / @./README.md | [email protected] 等は誤検知しない(行頭 or 空白後の @docs/・@CLAUDE.local.md・@./ のみ対象) |
スキャン対象:
- ソースコード(
.js/.gs/.ts/.tsx/.html): PropertiesService + scriptId 検査 CLAUDE.md/CLAUDE.local.md(任意のサブディレクトリ含む):@import 検査.md(上記以外)/.jsonなどは対象外
@ import 検知ロジックの根拠:
humanlayer の CLAUDE.md 指針「@ import は起動時に全展開され context を圧迫する」を技術的に強制。
PR #511 で導入後、PR #514 で @ を 19 箇所削除して context を 495,492 → 0 chars に削減した経緯を踏まえ、
新規混入を post_edit_guard.sh で阻止 (PR #515)。Progressive Disclosure (CLAUDE.md は索引、docs/ は図書館) の運用に統一。
注: 設定変更後に許可/拒否を変えたい場合は scripts/hooks/pre_bash_guard.sh または scripts/hooks/post_edit_guard.sh、.claude/settings.json の permissions.deny を編集する。
既存違反 (テクニカルデット) — クリーンアップ完了
2026-05-07 時点の PropertiesService.getScriptProperties() 直呼び 4 件は全て Env モジュール経由に置換済み:
| 旧違反箇所 | Env API 置換 |
|---|---|
mas/100_config/101_sys_config.js:1074, 1705(schema hash) | Env.lastSchemaHash() / Env.setLastSchemaHash() |
mas/800_ops/803_sync_tool.js:55(prod→dev 同期) | Env.spreadsheetId() (既存) + Env.prodSpreadsheetId() (新設) |
mas/800_ops/811_audit_checker.js:42, 126(N-30 監査) | Env.n30LastCheckedAt() / Env.setN30LastCheckedAt() |
以後は PostToolUse フックが新規違反の混入を技術的に阻止する。新たに PropertiesService の直呼びが必要なケースが出てきたら、Env モジュール (mas/000_infra/001_env.js) に専用のラッパー関数を追加する運用を維持する。
トラブルシュート
Hook が動かない
.claude/settings.jsonが存在し JSON valid か確認:node -e "JSON.parse(require('fs').readFileSync('.claude/settings.json','utf8'))"- スクリプトが実行可能か確認:
ls -l scripts/hooks/pre_bash_guard.sh(-rwxr-xr-x期待) - Claude Code 再起動を実施したか
$CLAUDE_PROJECT_DIRが解決されているか(設定ファイル内のパスが絶対参照になっているか)
誤検知が出る
pre_bash_guard.sh末尾の各if echo "$COMMAND" | grep -qE '...'; then block ...ブロックを必要に応じて緩める- ホワイトリスト的にコマンド先頭一致で許可したい場合は
block直前に early-return を追加
本当に必要な破壊的操作を実行したい
- フックを一時的に無効化するのではなく、Claude Code の外(手動ターミナル)で実行する運用が安全
- 例:
git reset --hardは手動 git 操作で。Claude には完了後の git 状態だけ伝える
更新ルール
フック追加時:
pre_bash_guard.shまたはpost_edit_guard.shに検知ブロックを 1 件追加 → このドキュメントの表に行追加静的パターン追加時:
settings.example.jsonのpermissions.denyに追記 → 既存ワークスペースは手動で.claude/settings.jsonを同期スクリプトのテストはローカルで以下のように実行可能:
# PreToolUse (Bash) echo '{"tool_input":{"command":"git push origin main"}}' | bash scripts/hooks/pre_bash_guard.sh echo $? # 期待: 2 # PostToolUse (Edit) echo '{"tool_name":"Edit","tool_input":{"file_path":"/tmp/x.js","old_string":"","new_string":"PropertiesService.getScriptProperties()"}}' \ | bash scripts/hooks/post_edit_guard.sh echo $? # 期待: 2
外部 API 書込の確認ゲート (COM-0381)
pre_bash_guard.sh は外部 API 書込 (curl/wget の -X POST/PUT/PATCH/DELETE・--request/--method・--upload-file) を
検知すると、ハードブロック (exit 2) でなく permissionDecision=ask をハーネスへ返し、操作単位のユーザー確認を要求する。
包括的「yes」を外部書込の承認とみなす拡大解釈 (ADR-0115 §1.2 の実害③) を技術的に遮断する。Claude は token 等で自己バイパスできない。
- 除外:
gh(GitHub・別認証/監査)・localhost/127.0.0.1 (ローカル dev)。-X(大文字) で proxy-xと区別。 - 動作確認 (payload 例):
# 外部書込 → ask (permissionDecision JSON を出力) printf '{"tool_input":{"command":"curl -X POST https://saas.example.com/api -d x=1"}}' \ | bash scripts/hooks/pre_bash_guard.sh # → {"hookSpecificOutput":{...,"permissionDecision":"ask",...}} # GET / localhost / gh は素通り (出力なし・exit 0) - jq 不在環境では ask JSON を組めないため安全側でハードブロック (exit 2) に倒す。
InstructionsLoaded ロードログ (ADR-0129 KPI-7)
InstructionsLoaded フック (scripts/hooks/instructions_loaded_log.sh) が、CLAUDE.md / .claude/rules/*.md /
nested CLAUDE.md のロードを .claude/logs/instructions_loaded.log (gitignored) に 1 行 JSON で記録する
(file_path + load_reason)。これで path-scoped ロードの無音失効を観測できる。
- CI 静的 lint (PR ゲート):
node scripts/adr0129-rules-loading-lint.mjs— gas.md のpaths:健全性 (ベース dir 実在・GAS レイヤーのカバー漏れ) と InstructionsLoaded フック配線を検証。実 claude は回さない。 - ローカル/定期チェック:
node scripts/adr0129-loadlog-check.mjs— ログに gas.md のload_reason=path_glob_match実績があるか確認 (exit 0=healthy / 1=未確認 / 2=ログ未採取)。0 件なら GAS ファイルを 1 つ Read するセッションを 流して再実行 → それでも 0 件なら失効を疑いtasks/adr0129/kpi6_paths_verification_log_2026-06-09.mdの決定的再検証へ。 - ロード判定は本フックログが唯一の権威。debug ログ grep や un-primed self-report は偽陰性 (memory: claude-rules-loading-verification)。
SessionStart 受信箱注入(運用プロンプトの自動表示)
SessionStart フック (scripts/hooks/session_start_prompts.sh) が、セッション開始時に tasks/prompts/ 直下の自分の役割宛てプロンプト(新しい順 6 本+総数。見出しは定型前置きを除いて 60 字まで)を <tasks-prompts-inbox> としてセッション文脈へ自動注入する。「handover を読んで」と言わなくても着信に気づける。本数・見出し長はセッション開始時の context 消費を抑えるため絞っている(2026-06-12 に 12 本×80 字 → 6 本×60 字へ変更)。
役割判定の規則: クローン名で自動判定する。
-doc/-subサフィックスと web パスは sub、それ以外は main。sub には*_to_sub/sub_*/handover_*だけが表示される。matcher:
startup|resume|clear|compact。再開 (resume) でも発火する(当初startup|clear|compactで再開時に出ない取りこぼしがあり修正済み)。着信通知 Actions と対:
.github/workflows/ops-prompt-notify.ymlが main への新着プロンプトを GitHub issue(label:ops-prompt)で通知し、tasks/prompts/archive/への退避(= 消費・ADR-0134)を検知して自動 close する。受信箱注入が「読む側」、通知 Actions が「届いたことを知らせる側」。テンプレ同期手順は従来どおり: 初回セットアップの手順 1(
settings.example.jsonをコピー)に含まれる。既存ワークスペースはSessionStartブロックを.claude/settings.jsonへ手動同期する。動作確認(matcher と無関係に中身を確認できる):
CLAUDE_PROJECT_DIR=$(git rev-parse --show-toplevel) bash scripts/hooks/session_start_prompts.sh # → role="sub" のクローンでは sub 宛てだけが出る
Stop context 警告(malformed 連鎖の予防)
Stop フック (scripts/hooks/context_budget_warn.sh) が、各ターン終了時に現在の context トークン量を測り、危険帯に入ったら handover / 新セッション再開を促す警告(systemMessage)を出す。
なぜ: ツール呼び出しの malformed(
"Your tool call was malformed and could not be parsed")は context サイズに強く相関する。実測(全 transcript 横断)で 34 件 / 12 セッション・cache_read≥50k 帯に集中し、1 セッションに 13 件・6 件の連鎖が起きる。根本原因は上流(モデルのツール呼び出しシリアライズ)で塞げないが、context を危険帯に入れない運用で「連鎖による高コスト」をほぼ消せる。本フックは人の記憶に頼らず自動で促す歯止め。しきい値:
WARN_AT=150000トークンで初回警告、以降STEP=50000刻みで 1 回ずつ。session_idごとに/tmpマーカーで抑制し、compact等で context が下がったらマーカーをリセットして再警告できる。context の算出:
transcript_path末尾 200 行から最新 assistant のusage(cache_read + cache_creation + input)を読む(全文走査を避け軽量化)。jqが無ければ無動作。安全性: 常に
exit 0。stop は決してブロックしない(無限ループ防止)。閾値未満や transcript 不在では無出力。テンプレ同期: 既存ワークスペースは
Stopブロックを.claude/settings.jsonへ手動同期する(settings.example.json参照)。動作確認:
# 160k 相当の擬似 transcript を渡して警告が出るか T=$(mktemp -d); printf '{"type":"assistant","message":{"usage":{"cache_read_input_tokens":160000,"cache_creation_input_tokens":0,"input_tokens":100}}}\n' > "$T/tr.jsonl" printf '{"session_id":"manualtest","transcript_path":"%s/tr.jsonl"}' "$T" | bash scripts/hooks/context_budget_warn.sh # → {"systemMessage":"⚠️ context 約 160k …"} が出れば OK関連: auto-memory
long-session-toolcall-glitch(2 型の切り分け・母集団データ・effort 特徴)。
関連ドキュメント
docs/_internal/git_workflow.md— Git 運用全般docs/dev/dev_mas-096_clasp_push_protection.md—.claspignore保護(git/clasp 層)docs/dev/dev_mas-195_pre_push_hook.md— git pre-push 自動チェック(git 層)scripts/pre-push-check.sh— 上記の実体