
こんにちは、サーバーワークスで生成AIの活用推進を担当している針生です。
本記事では、Claude Code に .env などの機密ファイルを読ませないようにする方法として、2 つのパターンを紹介します。
- 設定ファイルに拒否ルールを宣言的に書く permissions
- ツール実行直前にスクリプトを差し込んで判定する hooks
それぞれ単体でも使えますが、組み合わせることで多層に守れます。本記事では両方を順に取り上げ、最後に使い分けの目安をまとめます。
CLAUDE.md の指示だけでは不十分
.env をはじめとした機密ファイルを Claude Code に読ませると、セッションログに残ったりプロンプトインジェクションの対象になったりするリスクがあります。一方で、その対策として「CLAUDE.md に『.env を読まないで』と書く」だけでは不十分です。これは自然言語のお願いに過ぎず、文脈次第で守られないことがあります。
機密ファイルの保護は、自然言語の指示ではなく技術的に強制できる仕組みで担保するのが原則です。Claude Code には、この目的に使える permissions と hooks という機能が用意されているので、順に見ていきましょう。
permissions の deny ルールでシンプルにブロックする
最も手軽な方法は permissions の deny ルールです。プロジェクトの .claude/settings.json に次のように書きます。
{ "permissions": { "deny": [ "Read(.env)", "Read(.env.*)", "Bash(cat .env*)" ] } }
書き方のポイントは次のとおりです。
Read(...)の中身はファイルパターン。glob 形式が使えますRead(.env)のような素のファイル名は gitignore セマンティクスに従い、プロジェクト内のどの階層の.envもマッチしますBash(...)を加えると、Bash コマンド経由のアクセスにも備えられます
この設定をしておくと、Claude が .env を読み込もうとした時点で拒否され、ツールはそもそも実行されません。
設定ファイルを配置する場所は次のとおりです。
| 配置先 | 用途 |
|---|---|
~/.claude/settings.json |
全プロジェクトで共通の設定 |
.claude/settings.json |
プロジェクトでチーム共通のルール(git 管理対象) |
.claude/settings.local.json |
個人用の上書き(.gitignore 対象) |
.env の保護はチーム全体で共有すべきルールなので、プロジェクトの .claude/settings.json に書いておくのが基本です。
シンプルなパターンマッチで済む場合は、これだけで十分実用的です。ただし、deny ルールはあくまでファイル名やコマンドの文字列に対する一致判定です。たとえば「ファイル名は普通でも、中身に SECRET_KEY のような文字列が含まれていたら止めたい」といった、ファイルの内容に踏み込んだ判定はできません。また、ブロックした際にユーザーへ返す理由メッセージも固定で、文脈に合わせて出し分けることもできません。こうした柔軟性が欲しくなったときに登場するのが hooks です。
PreToolUse hook で細かく制御する
イベント一覧
hooks にはセッションやツール実行など、さまざまなイベントが用意されています。
- SessionStart / SessionEnd: セッションの開始・終了
- UserPromptSubmit: ユーザーがプロンプトを送信した直後
- PreToolUse: Claude がツールを実行する直前
- PostToolUse: ツール実行が成功した直後
.env の保護で使うのは PreToolUse です。Claude が Read や Bash のようなツールを呼び出す直前に介入し、ファイル名やコマンド、ファイルの中身までチェックして、該当すればブロックできます。
設定ファイルのフォーマット
PreToolUse の hook はこんな形で書きます。
{ "hooks": { "PreToolUse": [ { "matcher": "Read|Edit|Write|Bash", "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-env.sh", "args": [], "timeout": 5 } ] } ] } }
ネストが深いので、要素ごとに見ていきます。
- 外側の配列要素は
matcherとhooksを持ちます matcherは対象とするツール名を|区切りで指定します(正規表現としても解釈されます)- 内側の
hooks配列に、実際に呼び出すコマンドを書きます ${CLAUDE_PROJECT_DIR}はプロジェクトルートのパスに展開される組み込み変数ですtimeoutは秒単位で、長時間止まらないように 5 秒程度に設定しておくと安全です
スクリプトに渡される入力
hook スクリプトには標準入力経由で JSON が渡ってきます。Read ツールの場合はこんな内容です。
{ "session_id": "abc123", "cwd": "/home/user/my-project", "hook_event_name": "PreToolUse", "tool_name": "Read", "tool_input": { "file_path": "/home/user/my-project/.env" } }
tool_name が呼び出されようとしているツールの名前、tool_input の中身がそのツールへの引数です。ファイル系のツール(Read / Edit / Write)の場合は file_path、Bash の場合は command といった具合に、ツールごとに構造が異なる点に注意してください。
ブロックの仕方
スクリプトからは 2 つの方法で「ブロック」を伝えられます。
方法 1: 終了コードで伝える
exit 0: そのまま通すexit 2: ブロック。stderrに書いた内容が Claude にフィードバックされる
echo "機密ファイルへのアクセスはブロックされました" >&2 exit 2
方法 2: 標準出力に JSON を書く(exit 0 と併用)
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Access to .env files is blocked" } }
permissionDecision には allow / deny / ask を指定できます。理由を permissionDecisionReason に書いておくと、Claude にもユーザーにも何が起きたかが伝わりやすくなります。
シンプルにブロックするだけなら方法 1 が手軽です。一方、構造化された理由メッセージや細かい挙動制御が必要な場合は方法 2 が向いています。次節の実装例では、理由メッセージを明示的に渡したいので方法 2 を採用しています。
実装してみる
それでは、実際に .env をブロックする hook を作って動かしてみます。
ステップ 1: スクリプトを作成
プロジェクトルートで以下のファイルを作ります。
mkdir -p .claude/hooks touch .claude/hooks/block-env.sh chmod +x .claude/hooks/block-env.sh
.claude/hooks/block-env.sh の中身
#!/bin/bash
set -euo pipefail
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
command_str=$(echo "$input" | jq -r '.tool_input.command // empty')
target=""
case "$tool_name" in
Read|Edit|Write)
target="$file_path"
;;
Bash)
target="$command_str"
;;
esac
deny() {
jq -n --arg reason "$1" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: $reason
}
}'
exit 0
}
# 1. ファイル名・コマンドのパターンマッチ
if [[ "$target" =~ \.env(\.|$) ]]; then
deny "機密ファイルへのアクセスはブロックされました: $target"
fi
# 2. 中身に機密文字列が含まれていないかチェック(Read / Edit のみ)
if [[ "$tool_name" == "Read" || "$tool_name" == "Edit" ]] && [[ -f "$file_path" ]]; then
if grep -qE 'SECRET_KEY|PRIVATE_KEY|API_TOKEN' "$file_path"; then
deny "機密文字列を含むファイルへのアクセスはブロックされました: $file_path"
fi
fi
exit 0
スクリプトのポイントは次のとおりです。
jqで JSON をパースし、tool_nameとtool_inputから対象を抽出します- ツールによって対象が
file_pathだったりcommandだったりするので、caseで振り分けます - チェックは 2 段階で、まずファイル名・コマンドの正規表現マッチ、続いて Read / Edit のときに対象ファイルの中身を
grepして機密文字列の有無を確認します - ブロック時は前述の「方法 2」を採用し、
deny関数でpermissionDecision: "deny"を含む JSON を標準出力に書いてexit 0で終了します。exit 2は使っていませんが、JSON 側で deny を返しているため、Claude Code はツール呼び出しを停止します - どのチェックにもマッチしなければ何も出力せず
exit 0で通します - 本記事の例は bash ですが、hook の
commandは実行可能なファイルなら何でも指定できます。Node.js や Python など、慣れた言語で書いても問題ありません grep -qE 'SECRET_KEY|PRIVATE_KEY|API_TOKEN'のパターンは広めにマッチします。ドキュメントやサンプルコード内に該当文字列が含まれているとそれだけでブロックされるので、誤検知が気になる場合は対象ディレクトリを絞ったり除外パターンを足したりするとよいでしょう
ステップ 2: 設定ファイルに hook を登録
.claude/settings.json に hook を追加します。先ほどの permissions と組み合わせると、次のような設定になります。
{ "permissions": { "deny": [ "Read(.env)", "Read(.env.*)", "Bash(cat .env*)" ] }, "hooks": { "PreToolUse": [ { "matcher": "Read|Edit|Write|Bash", "hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-env.sh", "args": [], "timeout": 5 } ] } ] } }
ステップ 3: 動作確認
Claude Code を再起動してから、.env の中身を見せて と頼んでみます。.env を読もうとした時点で、permissions の deny ルールと hook の両方が該当するため、ツール呼び出しは拒否され、Claude は「機密ファイルのため読めません」といった応答を返します。Bash(cat .env) のような迂回も Bash(cat .env*) ルールと hook の両方が反応するので同様にブロックされます。
permissions のパターンに引っかからない経路、たとえば任意の名前のファイルに SECRET_KEY=... が書かれているケースは、hook 側のスクリプトが中身を読んで拾います。試しに config.txt のような無害な名前のファイルに SECRET_KEY=dummy と書き、Claude に読み込みを依頼してみてください。permissionDecisionReason に書いた「機密文字列を含むファイルへのアクセスはブロックされました」というメッセージがそのまま拒否理由として返ってくれば、中身チェックが期待どおり動いています。
permissions と hooks の使い分け
実用面では使いどころが違うので、観点ごとに比べてみます。
| 観点 | permissions の deny | PreToolUse hook |
|---|---|---|
| 設定の単純さ | ◎ JSON 数行 | △ スクリプトが必要 |
| パターンの柔軟性 | ○ glob 対応 | ◎ 任意のロジック |
| 監査・ログ | △ | ◎ 任意のログ出力可能 |
| 動的判定 | × | ◎ |
実用的には次のように分担するとよいでしょう。
- permissions の deny: ファイル名で機械的に止められるもの(
.env,.env.localなどのパターン) - PreToolUse hook: ファイル名だけでは判断できないケース(ファイルの中身を見たい、複数条件を組み合わせたい)や、ブロック時に独自のメッセージやログを残したい場合
本記事の実装例では、ファイル名のパターン判定とファイル内容の機密文字列チェックの 2 段階を hook で組み合わせました。同じ枠組みのまま、ログ書き出しを足したり、検出パターンを増やしたりといった拡張も無理なく行えます。
まとめ
- まずは permissions の deny ルールでファイル名ベースのブロックを設定する
- より細かい条件(ファイル内容のチェックなど)が必要な場合は PreToolUse hook で補強する
- チームで共有するために、設定はプロジェクトの
.claude/settings.jsonに書いて git で管理する
機密情報の取り扱いは、事故が起きてから気づくと取り返しがつかない類のテーマです。プロジェクトを始めるタイミングで permissions と hooks を設定しておけば、Claude Code を安心して使い続けられますね。
参考
- Claude Code Hooks — hooks の公式リファレンス
- Claude Code Permissions — permissions の評価順序や rule 構文
- Claude Code Settings — 設定ファイルの場所と優先順位
- Claude Code Security — セキュリティモデル全般
針生 泰有(執筆記事の一覧)
サーバーワークスで生成AIの活用推進を担当