Claude Code に .env などの機密ファイルを読ませない設定ガイド — permissions と hooks の 2 パターン

記事タイトルとURLをコピーする

Claude Code に .env などの機密ファイルを読ませない設定ガイド — permissions と hooks の 2 パターン

こんにちは、サーバーワークスで生成AIの活用推進を担当している針生です。

本記事では、Claude Code に .env などの機密ファイルを読ませないようにする方法として、2 つのパターンを紹介します。

  • 設定ファイルに拒否ルールを宣言的に書く permissions
  • ツール実行直前にスクリプトを差し込んで判定する hooks

それぞれ単体でも使えますが、組み合わせることで多層に守れます。本記事では両方を順に取り上げ、最後に使い分けの目安をまとめます。

CLAUDE.md の指示だけでは不十分

.env をはじめとした機密ファイルを Claude Code に読ませると、セッションログに残ったりプロンプトインジェクションの対象になったりするリスクがあります。一方で、その対策として「CLAUDE.md に『.env を読まないで』と書く」だけでは不十分です。これは自然言語のお願いに過ぎず、文脈次第で守られないことがあります。

機密ファイルの保護は、自然言語の指示ではなく技術的に強制できる仕組みで担保するのが原則です。Claude Code には、この目的に使える permissionshooks という機能が用意されているので、順に見ていきましょう。

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 が ReadBash のようなツールを呼び出す直前に介入し、ファイル名やコマンド、ファイルの中身までチェックして、該当すればブロックできます。

設定ファイルのフォーマット

PreToolUse の hook はこんな形で書きます。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Edit|Write|Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-env.sh",
            "args": [],
            "timeout": 5
          }
        ]
      }
    ]
  }
}

ネストが深いので、要素ごとに見ていきます。

  • 外側の配列要素は matcherhooks を持ちます
  • 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_pathBash の場合は 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_nametool_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 で管理する

機密情報の取り扱いは、事故が起きてから気づくと取り返しがつかない類のテーマです。プロジェクトを始めるタイミングで permissionshooks を設定しておけば、Claude Code を安心して使い続けられますね。

参考

針生 泰有(執筆記事の一覧)

サーバーワークスで生成AIの活用推進を担当