Claude Code Hooks で AI の暴走を止める|.env 読み込み拒否から自動 lint まで
許可プロンプトに頼る怖さ
Claude Code は、ファイルを読み書きし、コマンドを実行し、Git 操作まで自動でこなしてくれます。便利な反面、任せすぎると怖い場面があります。
.envをうっかり読まれて、API キーが会話ログに載ってしまったrm -rf distのつもりがrm -rf .で、プロジェクトごと消えたgit push --forceされて、チームメンバーのコミットが吹き飛んだ- 本番用の設定ファイルを書き換えられて、デプロイしたらサイトが落ちた
「Claude Code には許可プロンプトがあるから大丈夫でしょ?」と思うかもしれません。確かに、ファイルの書き込みやコマンド実行の前には「これを実行してもいいですか?」と聞いてきます。
ただ、長時間一緒に作業していると、そのプロンプトは何十回、何百回と出ます。集中していると「Y → Enter」を連打してしまう。私はしていました。人間の注意力に頼ったセキュリティは、いつか必ず破綻します。
しかも許可プロンプトは、危険な操作も無害な操作も同じ見た目で並びます。毎回同じ画面を見ているうちに、目が「内容を読む」から「押す」に切り替わってしまう。これは本人の注意力が足りないのではなく、人間の仕組みとしてそうなるものです。だから、読む側を鍛えるより、読まなくても事故が起きない側を用意するほうが筋が通っています。
交通事故を減らすのに「気をつけましょう」の標語だけでは足りず、ガードレールや信号機が要るのと同じで、ヒューマンエラーは気合いで防ぐものではなく、起きない仕組みにしておくものです。その仕組みが Hooks(フック)です。
Hooks を使うと、ツール実行の前後にシェルスクリプトを自動で挟み込み、危険な操作をプログラムでブロックできます。Y を連打しようが寝ぼけていようが、スクリプトが exit 1 を返せば操作は実行されません。
英語圏では Reddit や X で設定例が出回り始めていますが、日本語の解説はほとんど見かけません。基本の考え方から、実際に使えるレシピまで、順に見ていきます。
🚨 危険
本記事に掲載しているスクリプトには AI によって生成されたコードが含まれています。あくまで考え方と導入の参考例であり、そのまま使えば完璧なセキュリティ対策になるわけではありません。ご自身の環境に合わせて検証・カスタマイズしたうえでご利用ください。
Hooks とは
Hooks は Claude Code のイベント駆動型フィルターです。「Claude がファイルを読もうとした」「コマンドを実行しようとした」「ファイルを書き込もうとした」。こうしたイベントが起きるたびに、あなたが書いたシェルスクリプトが自動で走ります。
空港のセキュリティゲートを思い浮かべると分かりやすいです。旅客(Claude)が搭乗口(ファイル操作)に向かうと、ゲート(Hook)が自動でスキャン(スクリプトの実行)をかけます。問題なければ通過(exit 0)、危険物が見つかればブロック(exit 1)。旅客がどれだけ急いでいても、ゲートは例外なくチェックします。
設定は .claude/settings.json か .claude/settings.local.json に書きます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"command": "bash .claude/hooks/block-env.sh"
}
]
}
}
「いつ」「何のツールで」「何を実行するか」を宣言的に書くだけです。一度書けば、あとは Claude Code が毎回チェックしてくれます。
実際の処理は、この例のように別のシェルスクリプトへ切り出しておくのがおすすめです。settings.json には「どのタイミングで、どのスクリプトを呼ぶか」だけが並ぶので見通しがよくなりますし、スクリプト単体で動作を確かめられます。この記事のレシピも、すべてその形で書いています。
CLAUDE.md だけでは足りない理由
「.env を読まないで」と CLAUDE.md に書いておけばいいのでは、と思うかもしれません。実際、CLAUDE.md に書いたルールは Claude もかなり守ってくれます。ただ 100% ではありません。AI は確率的なモデルなので、会話が長くなるとコンテキストの中で指示が薄れ、ルールを忘れることがあります。
CLAUDE.md はお願いベースで、AI が読んで判断するので忘れることもあります。Hooks は強制ベースで、プログラムが毎回チェックするので例外がありません。
私は「CLAUDE.md でガイドし、Hooks でガードする」という二層構造が一番しっくりきます。CLAUDE.md は「こうしてほしい」という方針、Hooks は「これだけは絶対にやらせない」という最後の一線。役割が違うので、両方あって初めて安心できます。
逆に言うと、Hooks に全部を背負わせる必要もありません。「こういう書き方が好み」といったスタイルの話まで Hooks で縛ると、設定が増えて自分の首を絞めます。Hooks に回すのは、起きたら取り返しがつかないこと、あるいは起きるたびに困ることだけ。それ以外は CLAUDE.md で十分です。
3 種類のフック
Hooks には 3 つのタイミングがあります。
| フック | タイミング | 主な用途 |
|---|---|---|
| PreToolUse | ツール実行の 前 | 危険な操作のブロック・確認 |
| PostToolUse | ツール実行の 後 | 自動 lint・フォーマット・通知 |
| UserPromptSubmit | ユーザーが入力を送信した時 | 入力の前処理・ログ記録 |
PreToolUse:実行前にブロックできる
一番よく使うのがこれです。Claude がツール(Read、Write、Edit、Bash など)を使おうとした瞬間に発火します。
他の 2 つは事後処理と入力処理ですが、PreToolUse だけは実行そのものを止められます。.env を読ませない、rm -rf を実行させない。やってはいけないことを物理的に止められるのは、PreToolUse だけです。
動きはこうなります。
- Claude が「
Readでファイルを読もう」と判断する - 実行前に PreToolUse フックが発火する
- あなたのスクリプトに、何のツールで何をしようとしているかが JSON で渡される
- スクリプトが判定する
exit 0なら許可。Claude は操作を続行するexit 1ならブロック。stderr に書いた理由が Claude にフィードバックされる
- ブロックされた場合、Claude はその理由を読んで別のアプローチを考える
たとえば .env の読み込みをブロックすると、Claude は「.env は読めないので、代わりに .env.example を参考にしましょう」と提案してくれます。ただ止まるのではなく、ブロックの理由を読んで回避策まで考えてくれるところが賢いです。
この性質があるので、PreToolUse は「止める」だけでなく「正しい方向へ誘導する」道具としても使えます。ブロックメッセージは、人間への警告ではなく Claude への指示書だと思って書くと、うまくいきます。
PostToolUse:実行後に自動処理
ツールの実行が終わったあとに発火します。PreToolUse が門番なら、PostToolUse は後片付け係です。
使いどころはこのあたりです。
- Claude が書いたファイルを Prettier / ESLint で自動フォーマットする
- ファイルを書き換えた直後に、関連するテストを自動で走らせる
- 「いつ、どのファイルが、どう変更されたか」をログに残す
- Slack に「Claude がコミットしました」と通知を飛ばす
PostToolUse のスクリプトが失敗(exit 1)しても、操作自体は取り消されません。あくまで実行後の追加処理です。ここが PreToolUse との違いです。
UserPromptSubmit:ユーザー入力時
プロンプトを送信したタイミングで発火します。
- 自分が何を指示したかをファイルに記録しておく(あとで振り返りたい時に便利)
- 「本番」「production」「delete」などの危険な単語が含まれていたら一拍置く
- 短縮記法を正式な指示に展開する
3 つの中では使う機会が一番少なめです。ただ、チーム運用で操作の監査ログを残したい場面では重宝します。
この記事の実践レシピは、PreToolUse と PostToolUse だけで組んであります。まずはこの 2 つを押さえれば、実用上は十分です。
設定方法
2 つの設定ファイル
| ファイル | 用途 | Git 管理 | 優先度 |
|---|---|---|---|
.claude/settings.json | チーム共有の設定 | する | 低 |
.claude/settings.local.json | 個人用の設定 | しない | 高(上書き) |
使い分けの目安は次のとおりです。
.claude/settings.json は、チーム全員に適用したいものを置きます。
.envファイルの読み込みブロックrm -rfやgit push --forceのブロック- CI/CD 設定ファイルの書き込み保護
.claude/settings.local.json は、個人の好みを置きます。
- 自動フォーマット(Prettier 派か ESLint 派か)
- 操作ログの保存
- 通知設定
両方に同じフックタイプの設定がある場合は、local が優先されます。
迷ったら、まず個人用の settings.local.json で試し、問題なく動くと確かめてから settings.json に移すと安全です。チーム全員に効く設定を、動作未確認のまま入れてしまうと、全員の作業を止めかねません。
JSON の基本構造
{
"hooks": {
"PreToolUse": [
{
"matcher": "ツール名",
"command": "実行するコマンド"
}
],
"PostToolUse": [
{
"matcher": "ツール名",
"command": "実行するコマンド"
}
],
"UserPromptSubmit": [
{
"command": "実行するコマンド"
}
]
}
}
matcher にはツール名を指定します。Claude Code が使う主なツールは次のとおりです。
| matcher | 対象 | 例 |
|---|---|---|
Read | ファイル読み込み | .env や設定ファイルの保護 |
Write | ファイル新規作成 | 意図しないファイル生成の防止 |
Edit | ファイル編集 | 重要ファイルの書き換え防止 |
Bash | コマンド実行 | 危険コマンドのブロック |
Glob | ファイル検索 | 特定ディレクトリの探索防止 |
Grep | テキスト検索 | 秘密情報の検索防止 |
1 つのフックタイプに複数のルールを配列で書けるので、用途ごとにスクリプトを分けて管理できます。
フックスクリプトが受け取る JSON
フックスクリプトには、Claude が実行しようとしているツールの入力情報が、標準入力(stdin)から JSON で渡されます。
#!/bin/bash
# stdin から JSON を読み取る
input=$(cat)
# 例:Read ツールの場合のJSON
# {
# "tool_name": "Read",
# "tool_input": {
# "file_path": "/home/user/project/.env"
# }
# }
# ファイルパスを取り出す
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
echo "Claude が $file_path を読もうとしています" >&2
Bash ツールの場合はこうなります。
# {
# "tool_name": "Bash",
# "tool_input": {
# "command": "rm -rf dist/"
# }
# }
command=$(echo "$input" | jq -r '.tool_input.command // empty')
echo "Claude が実行しようとしているコマンド: $command" >&2
Edit ツールの場合です。
# {
# "tool_name": "Edit",
# "tool_input": {
# "file_path": "src/index.ts",
# "old_string": "古い文字列",
# "new_string": "新しい文字列"
# }
# }
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
new_string=$(echo "$input" | jq -r '.tool_input.new_string // empty')
終了コードのルール
| 終了コード | 意味 | PreToolUse での効果 | PostToolUse での効果 |
|---|---|---|---|
0 | 成功 | 操作を許可 | なし(正常終了) |
0 以外 | 失敗 | 操作をブロック | 警告のみ(操作は取り消されない) |
PreToolUse でブロックしたとき、stderr に書いた内容が Claude にフィードバックされます。なぜブロックしたのかを書いておくと、Claude がちゃんと代替案を考えてくれます。理由を書かずに exit 1 だけ返すと、Claude は何がいけなかったのか分からず、同じことを繰り返しがちです。メッセージには「何がだめだったか」と「代わりに何をすればよいか」の両方を入れておくと、やり直しが早く済みます。
# 悪い例(理由がない)
exit 1
# 良い例(理由が明確)
echo "BLOCKED: .env ファイルは秘密情報を含むため読み込み禁止です。.env.example を参照してください。" >&2
exit 1
実践レシピ集
ここからは、そのままコピペして使えるレシピです。レシピごとに、なぜ必要か、コード、動作の流れ、カスタマイズのヒントを載せています。
レシピ 1:.env ファイルの読み込みをブロック
.env ファイルには、たとえば次のような情報が入っています。
DATABASE_URL=postgres://user:password@host:5432/mydb
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxx
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxx
STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxx
Claude Code に「プロジェクトの構成を見て」と頼んだだけで、.env を読んでしまうことがあります。内容は会話ログに残るので、万が一ログが漏れたら、サービスの認証情報がまとめて流出しかねません。
たかが 1 ファイル、されど 1 ファイル。しかも漏れたキーは、気づいたときにはもう使われていることがあります。API キーの漏洩は数分で数万円の被害につながることもあります(AWS のクレデンシャルが漏れて仮想通貨のマイニングに使われた事例は有名です)。
.claude/hooks/block-env-read.sh:
#!/bin/bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
# .env で始まるファイル名をすべてブロック(.env, .env.local, .env.production 等)
if [[ "$(basename "$file_path")" == .env* ]]; then
echo "BLOCKED: .env ファイルの読み込みは禁止されています。環境変数の参照には .env.example を使ってください。" >&2
exit 1
fi
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"command": "bash .claude/hooks/block-env-read.sh"
}
]
}
}
動作の流れは次のとおりです。
- あなた:「このプロジェクトの構成を見て」
- Claude:「まず
.envを読んで環境変数を確認します」 - Hook が発火し、
.envを含むパスを検出してexit 1 - Claude:「
.envは読み込み禁止のようです。代わりに.env.exampleを確認しますね」
ブロック理由に「.env.example を使ってください」と書いてあるので、Claude は自然に安全な代替案へ切り替えてくれます。
スクリプトは、ファイル名が .env で始まるかどうかで判定しています。.env.local や .env.production のような派生ファイルも同じ網にかかるので、本番用の値が入りがちなファイルを個別に書き並べなくて済みます。反対に、.env.example まで止めてしまうと代替案が消えてしまいます。実際の運用では、ここが一番迷うところです。
.env だけでなく、credentials.json や secrets.yaml なども対象にしたい場合は、こう書き換えます。
blocked_files=(".env" "credentials" "secrets" ".pem" ".key")
basename_file=$(basename "$file_path")
for blocked in "${blocked_files[@]}"; do
if [[ "$basename_file" == *"$blocked"* ]]; then
echo "BLOCKED: $basename_file は機密ファイルのため読み込み禁止です" >&2
exit 1
fi
done
レシピ 2:危険なコマンドをブロック
Claude Code は Bash コマンドを実行できます。ほとんどの場合は適切なコマンドを選んでくれますが、文脈を誤解して破壊的なコマンドを出してくることがあります。起こりうる例を挙げます。
- 「ビルド成果物をきれいにして」→
rm -rf .(カレントディレクトリ全消し) - 「このブランチの変更をリモートに反映して」→
git push --force(他人のコミットを上書き) - 「テスト用のDBをリセットして」→
DROP DATABASE production;(本番DB消去)
許可プロンプトでコマンドの中身を注意深く読めればいいのですが、長いコマンドだと見落としがちです。Hooks なら、機械的にパターンマッチで弾けます。
.claude/hooks/block-dangerous.sh:
#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
# 危険パターンのリスト(必要に応じて追加・削除)
dangerous_patterns=(
"rm -rf /"
"rm -rf ."
"rm -fr"
"rm -rf ~"
"git push --force"
"git push -f"
"git reset --hard"
"git clean -fd"
"git checkout -- ."
"git branch -D"
"DROP TABLE"
"DROP DATABASE"
"TRUNCATE"
":(){ :|:& };:"
"mkfs"
"dd if="
"> /dev/sda"
)
for pattern in "${dangerous_patterns[@]}"; do
if [[ "$command" == *"$pattern"* ]]; then
echo "BLOCKED: 危険なコマンドパターン '$pattern' が含まれています。本当に必要な場合は、手動でターミナルから実行してください。" >&2
exit 1
fi
done
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "bash .claude/hooks/block-dangerous.sh"
}
]
}
}
動作の流れです。
- あなた:「dist フォルダをきれいにして」
- Claude:「
rm -rf dist/を実行します」→ 通過(dist/限定なので安全) - あなた:「全部きれいにして」
- Claude:「
rm -rf .を実行します」→ ブロック - Claude:「危険なコマンドがブロックされました。代わりに
git clean -nで確認してから削除しましょうか?」
rm -rf dist/ のような限定的な削除は通し、rm -rf . のような全消しだけを止める。この粒度は、パターンの書き方で調整できます。ただし、このスクリプトは文字列が含まれているかを見るだけなので、rm -rf ./dist のように先頭が rm -rf . と一致するコマンドも止まります。逆に、同じ意味でも書き方を変えられると通り抜けてしまいます。完璧な防御ではなく、よくある事故を機械的に減らす網だと割り切って使うのがちょうどいいです。止まりすぎて作業に支障が出たら、そのパターンを外せばよいだけです。
追加すべきパターンはプロジェクトによって違います。Rails なら rails db:drop、Docker なら docker system prune -af、npm なら npm publish(意図しない公開を防ぐ)を足しておくと安心です。
レシピ 3:特定ディレクトリへの書き込みを禁止
プロジェクトの中には、Claude に触らせたくないファイルがあります。
.github/workflows/:CI/CD パイプラインの設定。書き換えられると、デプロイが壊れるか、意図しないコードが本番に流れるterraform/、infrastructure/:インフラの定義。間違えるとサーバーが消えるdocker-compose.yml:コンテナ構成。ポート開放やボリューム設定を変えられると危険package.jsonの scripts:悪意あるスクリプトが紛れ込む可能性
Claude Code は「良かれと思って」これらを書き換えることがあります。たとえば「CI が遅いので最適化しますね」と workflow ファイルを変更して、結果的にテストをスキップする設定にしてしまう、といった具合です。悪意はなく親切心から起きるので、気づきにくいのが厄介です。差分をよく見ない限り、何が変わったか分からないまま次の作業に進んでしまいます。
.claude/hooks/protect-dirs.sh:
#!/bin/bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.file // empty')
# 保護するパスのリスト
protected_dirs=(
".github/workflows"
"infrastructure/"
"terraform/"
"docker-compose"
"Dockerfile"
".gitlab-ci"
"Jenkinsfile"
)
for dir in "${protected_dirs[@]}"; do
if [[ "$file_path" == *"$dir"* ]]; then
echo "BLOCKED: $dir は保護されたパスです。変更が必要な場合は手動で編集してください。" >&2
exit 1
fi
done
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"command": "bash .claude/hooks/protect-dirs.sh"
},
{
"matcher": "Edit",
"command": "bash .claude/hooks/protect-dirs.sh"
}
]
}
}
保護する対象はプロジェクトの性質で変わります。Web アプリなら nginx.conf や Caddyfile、モノレポなら他チームのパッケージディレクトリ、モバイルアプリなら ios/ と android/ のネイティブ部分、このブログのような静的サイトなら .github/workflows/ あたりが候補です。
レシピ 4:ファイル編集後に自動フォーマット
Claude Code が書くコードは概ねきれいです。それでも、プロジェクトの Prettier / ESLint 設定とは微妙にずれることがあります。インデントがスペース 2 つのはずが 4 つで書かれたり、セミコロンのありなしが混在したり。
毎回 npx prettier --write を手で叩くのは面倒ですし、忘れます。PostToolUse でファイル保存のたびに自動フォーマットすれば、常にプロジェクトのスタイルガイドに沿ったコードになります。
.claude/hooks/auto-format.sh:
#!/bin/bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.file // empty')
# ファイルが存在しない場合(削除操作など)はスキップ
if [[ ! -f "$file_path" ]]; then
exit 0
fi
# JS/TS/JSX/TSX/CSS/JSON ファイルのみフォーマット
case "$file_path" in
*.js|*.ts|*.jsx|*.tsx|*.css|*.json|*.md|*.mdx)
npx prettier --write "$file_path" 2>/dev/null
;;
esac
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"command": "bash .claude/hooks/auto-format.sh"
},
{
"matcher": "Write",
"command": "bash .claude/hooks/auto-format.sh"
}
]
}
}
動作の流れです。
- Claude が
src/components/Header.tsxを編集 - PostToolUse フックが発火
npx prettier --write src/components/Header.tsxが自動実行- ファイルがプロジェクトのスタイルガイド通りに整形される
Claude が次にそのファイルを読むときには整形後の状態になっているので、以降の作業も一貫したスタイルで進みます。
注意点がひとつ。PostToolUse は実行のたびに走るので、Prettier の起動時間(数百ms〜数秒)が Claude の操作ごとに加算されます。プロジェクトが大きくて Prettier が遅い場合は、対象ファイルの拡張子を絞って対処してください。
フォーマットを Pre ではなく Post に置いているのは、整形は書き込みが終わったあとのファイルに対してかけるものだからです。また、このフックは失敗しても操作が取り消されないので、整形ツールが入っていない環境でも作業が止まりません。
レシピ 5:秘密情報の書き込みを検知
Claude に「API 連携のサンプルコードを書いて」と頼んだとき、プレースホルダーではなく、本物のキーっぽい文字列を生成することがあります。また、会話の中であなたが API キーを貼り付けてしまい、Claude がそれをコードにそのまま書き込む場合もあります。
このフックは、ファイルに書き込まれようとしている内容に秘密情報のパターンが含まれていないかをチェックします。
.claude/hooks/detect-secrets.sh:
#!/bin/bash
input=$(cat)
new_string=$(echo "$input" | jq -r '.tool_input.new_string // .tool_input.content // empty')
# 書き込み内容がない場合はスキップ
if [[ -z "$new_string" ]]; then
exit 0
fi
# 秘密情報のパターン(正規表現)
secret_patterns=(
"sk-[a-zA-Z0-9]{20,}" # OpenAI API key
"sk_live_[a-zA-Z0-9]+" # Stripe Secret key
"sk_test_[a-zA-Z0-9]+" # Stripe Test key
"AKIA[A-Z0-9]{16}" # AWS Access Key ID
"ghp_[a-zA-Z0-9]{36}" # GitHub Personal Access Token
"gho_[a-zA-Z0-9]{36}" # GitHub OAuth Token
"xoxb-[0-9]+-[a-zA-Z0-9]+" # Slack Bot Token
"-----BEGIN.*PRIVATE KEY-----" # SSH / TLS 秘密鍵
"AIza[a-zA-Z0-9_-]{35}" # Google API key
)
for pattern in "${secret_patterns[@]}"; do
if echo "$new_string" | grep -qE "$pattern"; then
echo "BLOCKED: 秘密情報のパターンが検出されました (pattern: $pattern)。API キーやトークンはコードにハードコードせず、環境変数を使ってください。" >&2
exit 1
fi
done
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"command": "bash .claude/hooks/detect-secrets.sh"
},
{
"matcher": "Write",
"command": "bash .claude/hooks/detect-secrets.sh"
}
]
}
}
動作の流れです。
- あなた:「OpenAI の API を呼び出すコードを書いて。キーは sk-abc123… を使って」
- Claude がコードに
sk-abc123...をハードコードしようとする - Hook が発火し、OpenAI のキーパターンを検出してブロック
- Claude:「API キーのハードコードがブロックされました。代わりに
process.env.OPENAI_API_KEYを参照するコードを書きますね」
頼まなくても、勝手に安全な書き方へ寄せてくれます。
秘密情報は、いったんコミットや公開に進んでしまうと、あとから消しても履歴に残ります。書き込まれる前の段階で止められるのは、この PreToolUse ならではの利点です。ただ、パターンに載っていない形式のキーは検知できないので、普段使うサービスの形式に合わせて足していってください。ブロックメッセージに「環境変数を使ってください」と書いてあるのが効いています。
レシピ 6:操作ログを記録する
Claude Code で長時間作業していると、「あれ、さっき何を変えたっけ?」となることがあります。複数のファイルを同時に編集しているときは特に、どのファイルをいつ変更したかの全体像がつかみにくくなります。チーム開発で「Claude に何を触らせたか」の記録を残しておきたい場合にも役立ちます。
.claude/hooks/log-operations.sh:
#!/bin/bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // "unknown"')
file_path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.file // empty')
command=$(echo "$input" | jq -r '.tool_input.command // empty')
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
log_file=".claude/operation.log"
if [[ -n "$file_path" ]]; then
echo "[$timestamp] $tool_name: $file_path" >> "$log_file"
elif [[ -n "$command" ]]; then
echo "[$timestamp] $tool_name: $command" >> "$log_file"
fi
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"command": "bash .claude/hooks/log-operations.sh"
},
{
"matcher": "Write",
"command": "bash .claude/hooks/log-operations.sh"
},
{
"matcher": "Bash",
"command": "bash .claude/hooks/log-operations.sh"
}
]
}
}
ログはこんな形で残ります。
[2026-04-13 14:30:22] Edit: src/components/Header.tsx
[2026-04-13 14:30:45] Bash: npm run build
[2026-04-13 14:31:10] Write: src/utils/helper.ts
[2026-04-13 14:31:33] Edit: src/pages/index.astro
[2026-04-13 14:32:01] Bash: git add -A && git commit -m "refactor header"
作業後にこのログを見返すだけで、Claude が何をしたかが一目で分かります。.gitignore に .claude/operation.log を追加しておくことをおすすめします。ログには作業中のコマンドがそのまま残るので、うっかりリポジトリに入れてしまわないための保険です。このレシピは止めるものではなく、あとから振り返るための記録係なので、他のレシピと違って入れても作業の邪魔になりません。
レシピ 7:特定ブランチへのコミットを禁止
main や production ブランチに直接コミットさせたくない場合のレシピです。Claude Code は指示すれば git commit も git push もしてくれますが、ブランチの確認をせずにコミットしてしまうことがあります。
.claude/hooks/protect-branch.sh:
#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
# git commit / git push を含むコマンドのみチェック
if [[ "$command" != *"git commit"* && "$command" != *"git push"* ]]; then
exit 0
fi
# 現在のブランチを取得
current_branch=$(git branch --show-current 2>/dev/null)
protected_branches=("main" "master" "production" "staging")
for branch in "${protected_branches[@]}"; do
if [[ "$current_branch" == "$branch" ]]; then
echo "BLOCKED: $branch ブランチへの直接コミット/プッシュは禁止されています。feature ブランチを作成してください。" >&2
exit 1
fi
done
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "bash .claude/hooks/protect-branch.sh"
}
]
}
}
これで、うっかり main で作業していても、コミットの瞬間にブロックされます。ブランチを切り替え忘れるのは、人間でもよくやるミスです。コミットを止めたあとで、ブランチを切って作業をやり直す流れまで Claude が自分で進めてくれるので、手戻りもほとんどありません。
おすすめの初期設定セット
レシピが多くて、どこから始めるか迷う方へ。段階的に入れていくのがおすすめです。一度に全部入れると、動かないときにどれが原因か分からなくなります。1 つ入れて、動作を確かめて、次に進む。その順番が結局いちばん早いです。
レベル 1:最低限の安全装置(5 分で設定)
まずはこれだけ。ありがちな事故はだいたいこれで防げます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"command": "bash .claude/hooks/block-env-read.sh"
},
{
"matcher": "Bash",
"command": "bash .claude/hooks/block-dangerous.sh"
}
]
}
}
必要なスクリプトは、block-env-read.sh(レシピ 1)と block-dangerous.sh(レシピ 2)です。
レベル 2:品質の自動維持(+5 分)
レベル 1 に、自動フォーマットと秘密情報の検知を足します。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"command": "bash .claude/hooks/block-env-read.sh"
},
{
"matcher": "Bash",
"command": "bash .claude/hooks/block-dangerous.sh"
},
{
"matcher": "Edit",
"command": "bash .claude/hooks/detect-secrets.sh"
},
{
"matcher": "Write",
"command": "bash .claude/hooks/detect-secrets.sh"
}
],
"PostToolUse": [
{
"matcher": "Edit",
"command": "bash .claude/hooks/auto-format.sh"
},
{
"matcher": "Write",
"command": "bash .claude/hooks/auto-format.sh"
}
]
}
}
レベル 3:チーム運用(+10 分)
レベル 2 に、ディレクトリ保護、ブランチ保護、操作ログを足します。チームで Claude Code を使うならここまで入れておきたいところです。
ここまで来ると、Y を連打しても安全という状態にかなり近づきます。最初に仕組みを作る手間はかかりますが、あとは放っておいてもガードが効き続けます。ただし、フックが増えるほど実行時間も積み上がるので、本当に必要なものから足していき、使わなくなったものは外していく運用が向いています。
フックのデバッグ方法
「フックを設定したのに動かない」「ブロックされるはずなのに通ってしまう」。そんなときの切り分けです。順番としては、スクリプト単体で動くか、次に Claude Code から呼ばれているか、最後に判定条件が合っているか、の順に確かめると原因を絞りやすくなります。いきなり Claude Code の画面だけを見て悩むと、どこで止まっているのか分からなくなります。
まずスクリプト単体でテストする
フックスクリプトは普通のシェルスクリプトなので、Claude Code を通さずに直接テストできます。ブロックしたい入力と、通したい入力の両方を試しておくと、止めすぎも通しすぎも見つかります。
# テスト用の JSON を stdin に渡して実行
echo '{"tool_name":"Read","tool_input":{"file_path":"/home/user/.env"}}' | bash .claude/hooks/block-env-read.sh
# 終了コードを確認(1 ならブロック成功)
echo $?
デバッグ出力を追加する
スクリプトの中で >&2 を付けて stderr に出力すると、Claude Code のログに表示されます。
echo "DEBUG: file_path = $file_path" >&2
echo "DEBUG: command = $command" >&2
よくあるミス
| 症状 | 原因 | 対処 |
|---|---|---|
| フックが動かない | jq がインストールされていない | jq --version で確認、なければインストール |
| フックが動かない | スクリプトに実行権限がない | chmod +x .claude/hooks/*.sh |
| フックが動かない | パスが間違っている | command のパスをプロジェクトルートからの相対パスで確認 |
| 全部ブロックされる | jq のフィルタが常に空文字を返している | `echo “$input” |
| 一部だけ通り抜ける | パターンマッチが不完全 | 大文字小文字、スペースの違いをチェック |
使うときの注意点
jq が必要
この記事のレシピはすべて jq(JSON パーサー)を使っています。ほとんどの開発環境には入っていますが、なければ追加してください。
# macOS
brew install jq
# Ubuntu / Debian
sudo apt install jq
# Windows (Scoop)
scoop install jq
# Windows (Chocolatey)
choco install jq
jq を使わず grep や sed だけで書くこともできます。ただ、JSON の構造を読むなら jq のほうが圧倒的に楽です。
実行時間
フックスクリプトはツール実行のたびに走るので、重い処理を入れると Claude Code の体感速度が落ちます。
| 処理 | 所要時間 | 影響 |
|---|---|---|
| jq でパターンマッチ | 数 ms | ほぼなし |
| Prettier(小さいファイル) | 200〜500 ms | 許容範囲 |
| ESLint(プロジェクト全体) | 数秒〜十数秒 | 遅すぎ |
| テストスイート全体 | 数十秒〜数分 | 論外 |
PostToolUse のスクリプトは 1 秒以内に収めたいところです。重い処理は「コミット時だけ」「特定のファイルだけ」と条件を絞れば対処できます。遅さは使っているうちにじわじわ効いてきて、気づいたときには Claude Code 自体を使うのが億劫になります。入れる前に、そのフックが毎回走ってよいほど軽いかを一度考えてみてください。
settings.json と settings.local.json の使い分け
settings.json は Git で管理されてチームに共有されるので、セキュリティやチーム共通のルール(.env ブロック、危険コマンドブロックなど)を置きます。settings.local.json は Git に入らない個人用なので、好みや環境依存のもの(自動フォーマット、操作ログなど)を置きます。
チームで使うなら、settings.json と hooks/ ディレクトリをリポジトリに含めて、PR レビューの対象にしましょう。新しいメンバーが Claude Code を使い始めた時点で、自動的にガードレールが効いている状態が理想です。
よくある質問
Hooks を設定したら Claude Code が遅くなりませんか?
PreToolUse のパターンマッチ程度なら、体感できないレベル(数 ms)です。PostToolUse で Prettier を走らせる場合は 200〜500 ms ほど加算されますが、コードが常にきれいになる利点のほうが大きいです。重い処理さえ避ければ問題ありません。
フックをバイパスする方法はありますか?
Claude Code 自体がフックをバイパスすることはできません。これが Hooks の強みです。ただし、あなた自身がターミナルで直接コマンドを実行すれば、当然フックは関係ありません。「Claude にはやらせないけど、自分ではやりたい」という操作は手動で実行してください。
Windows でも使えますか?
使えます。bash スクリプトは Git Bash や WSL で実行されます。Windows 環境の場合、command は次のように書くとよいです。
{
"command": "bash .claude/hooks/block-env-read.sh"
}
Git for Windows がインストールされていれば、bash コマンドは使えます。
1 つのツールに複数のフックを設定できますか?
できます。配列に複数のオブジェクトを並べるだけです。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "bash .claude/hooks/block-dangerous.sh"
},
{
"matcher": "Bash",
"command": "bash .claude/hooks/protect-branch.sh"
}
]
}
}
すべてのフックが exit 0 を返した場合だけ操作が許可されます。1 つでも exit 1 を返せばブロックです。
最初の 10 分で入れておく
Claude Code Hooks は、人間の注意力に頼らず、仕組みで安全を担保するための機能です。
設定が面倒、シェルスクリプトを書くのがだるい。その気持ちは分かります。でも、rm -rf . でプロジェクトが消えてから払う代償に比べれば、最初の 10 分でフックを入れるコストは安いものです。
まずはレシピ 1(.env ブロック)だけでも入れてみてください。設定ファイルを 1 つ置いて、スクリプトを 1 本書くだけです。「.env を読もうとしてもブロックされる」と分かっているだけで、Y を押す指がだいぶ気楽になります。
前回の git worktree で並列開発する記事 と合わせて、Claude Code をもっと安心して使い倒してみてください。
🚨 危険
本記事に掲載しているスクリプトには、AI によって生成されたコードが含まれています。あくまで Hooks の考え方と導入の参考例であり、これらをそのまま使えば完璧なセキュリティ対策になるわけではありません。実際のプロジェクトに導入する際は、ご自身の環境や要件に合わせて内容を検証・カスタマイズしたうえでご利用ください。セキュリティに関わる設定は、最終的にはご自身の責任で判断をお願いいたします。
関連記事
Claude Code カスタムスラッシュコマンドの作り方|毎回同じ指示を打つのをやめる
Claude Code を2〜3個同時に走らせる|git worktree で並列開発する海外流儀
Claude Codeで複数エージェントを作って議論させる方法|AI同士のディベートが面白い
Claude Chat と Claude Code の違い|どっちを使うべきか用途別に解説
ChatGPT vs Claude vs Gemini の比較|料金・機能・使い分け
Midjourney v7 のプロンプトの書き方|パラメーター一覧と ChatGPT・Gemini での作り方
ChatGPT・ClaudeでSuno AI用の歌詞とプロンプトを作る方法
Suno AI のタグ一覧|メタタグ・スタイルプロンプトの使い方
PaperMCとは?WindowsでJava版マイクラマルチサーバーを立てる方法