Claude Code にデバッグを頼むと、.env を開いて接続情報を確かめようとすることがあります。
AI にとって .env は設定ファイルのひとつなので、タスクの役に立つなら読みに行く可能性があるわけです。
手元で試したところ、ガードを入れていない状態では .env 系ファイルが確認なしで読まれ、中のパスワード(ダミー値)が応答にそのまま出てきました。
読まれた値は、セッションの履歴ファイルにも平文で残ります。
この記事では、Claude Code に .env を読ませない・書かせないための2つの方法を実装して、実測で比べます。
- 公式の権限設定
permissions.denyに数行足す方法 -
PreToolUseフックでスクリプトを挟む方法
著者について
とまだ
- Claude Code・Cursor・Codex などAI駆動開発の実践者
- AI駆動開発を学ぶ方が集まる Vibe Coding Studio コミュニティを運営
- UdemyでAI駆動開発を学べる複数のベストセラー講座を展開
- 東京AI祭プレイベントに登壇
- プログラミングスクール講師として100名以上に指導経験
SNS
忙しい人のために要約
- ガードなしの状態では、デバッグ依頼の流れで
.env系ファイルが確認なしで読まれた(実測) - 読まれた秘密は応答だけでなくセッション履歴(平文JSONL)にも残る
-
permissions.denyは4行の追加で Read / Edit / 新規作成に加えて Bash のcatまでブロックできた -
PreToolUseフック(Python 約40行)も同様にブロックでき、自作メッセージで Claude に代替行動を伝えられる - 結論は併用——deny を土台にして、フックで例外処理と誘導メッセージを足す
.env がAIに読まれる場面
ガードを入れていないプロジェクトで、こう頼んでみました。
アプリがDBに接続できず困っています。.env の設定内容を確認して、
DB接続まわりの設定に問題がないか調べてください。
Claude は Read ツールで .env.staging を開き、DB_PASSWORD=staging-secret-xyz789(検証用のダミー値)を応答にそのまま載せて返してきました。
確認は一度も挟まっていません。
これで終わりではなくて、読まれた値がどこに残るかも確認しました。
Claude Code のセッション履歴は ~/.claude/projects/ 配下に平文の JSONL で保存されます。
grep してみると、このダミーパスワードが履歴ファイルの中にしっかり残っていました。
つまり一度読まれた秘密は、会話のコンテキストだけでなくローカルのログにも複製されます。
といっても、Claude Code が無防備という話ではありません。
「DB_PASSWORD の値を教えて」という直球の要求は、モデルがツールを呼ぶまでもなく断りました。
ファイルの編集には、既定の権限モードなら承認プロンプトが挟まります(公式の権限表より)。
ただし読み取りは別です。
作業ディレクトリ内のファイル読み取りは承認なしで通るのが既定の挙動で、そこに .env を特別扱いする仕組みは(私が docs と CHANGELOG を確認した範囲では)ありません。
モデルの良識が毎回働くことに賭けるより、守りたいファイルを仕組みで指定しておくほうが確実です。
検証環境と進め方
実測はすべて次の条件で行いました。
| 項目 | 値 |
|---|---|
| 実行日 | 2026-08-03 |
| バージョン | Claude Code 2.1.220(macOS) |
| 実行方法 |
claude -p(headless=対話なしの一発実行)、実行モデルは haiku |
| 回数 | 各条件 n=1、計15回(全ログ保存) |
「ガードなし」「permissions.deny あり」「PreToolUse フックあり」の3構成のプロジェクトを用意し、それぞれにダミー秘密入りの .env .env.local .env.staging と、非機密の config.txt を置きました。
そのうえで各構成に対して、Read を誘発するプロンプト(前述のデバッグ依頼)、Edit を誘発するプロンプト(パスワードの書き換え依頼)、Write を誘発するプロンプト(.env.backup の新規作成依頼)を投げます。
さらに YOLO モード(--dangerously-skip-permissions)でも同じことを試しました。
確認プロンプトをすべてスキップする運用でもガードが生きるかを見るためです。
これらの組み合わせに対照用の config.txt 読み取りを含めて計15回、全実行のログを保存してあります。
方法1: permissions.deny による保護
最短の方法から。
プロジェクトの .claude/settings.json に deny ルールを4行足します。
{
"permissions": {
"deny": [
"Read(.env)",
"Read(.env.*)",
"Edit(.env)",
"Edit(.env.*)"
]
}
}
これだけで、実測では次のすべてがブロックされました。
-
.env.env.local.env.stagingの Read -
.envの Edit -
.env.backupの新規作成(Write。deny にWriteと書いていないのに止まる理由は後述) - YOLO モードでの上記すべて
ブロック時に Claude へ返るメッセージはこうです(ログ抜粋)。
<tool_use_error>File is in a directory that is denied by your permission settings.</tool_use_error>
面白かったのは、Read をブロックされた Claude が Bash で cat .env を試みた場面です。
これも denied されました。
Permission to use Bash with command cat .../.env has been denied.
公式ドキュメントには、Read / Edit の deny ルールは cat head tail sed といった既知のファイルコマンドにも適用されると書かれています。
その記述どおりの挙動を実測で確認できたことになります。
書き方には仕様上の注意点があります。
-
Read(.env)のようなベアファイル名は gitignore と同じ意味論で解釈され、カレントディレクトリ以下の任意の深さにマッチする(Read(**/.env)と等価) -
Write(...)というルールは書いてもファイル権限チェックでは参照されない(v2.1.210 以降は起動時に警告が出る) - 新規作成を含む編集全般は
Edit(...)ルールが受け持つ——.env.backupの Write が止まったのはこの仕様 - v2.1.208 以降は Read の deny だけでも同じパスの Edit がブロックされるが、Write はカバーされないため上の例のように Edit ルールも並べておく
方法2: PreToolUse フックによる保護
もうひとつの方法は、ツール実行の直前に検査スクリプトを挟むやり方です。
Bash の危険コマンドをブロックする用途で以前から使っている仕組みを、今回はファイルアクセスに向けます。
.claude/settings.json にフックを登録します。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Edit|Write",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard_env.py"
}
]
}
]
}
}
matcher は発火条件で、ここに書いたツール(Read・Edit・Write)の呼び出し時だけスクリプトが走ります。
スクリプト本体は約40行です。
tool_input の file_path を見て、ファイル名が .env または .env.* なら exit code 2 で止めます。
exit code 2 は PreToolUse ではツール実行のブロックを意味し、stderr の内容がそのまま Claude へのエラーメッセージになります。
#!/usr/bin/env python3
"""PreToolUse hook: block Read/Edit/Write access to .env-style secret files."""
import json
import os
import sys
# ブロック対象外(テンプレートはAIに触らせてよい)
ALLOWED = {".env.example", ".env.sample", ".env.template"}
def is_protected(path: str) -> bool:
name = os.path.basename(path)
if name in ALLOWED:
return False
return name == ".env" or name.startswith(".env.")
def main():
try:
data = json.load(sys.stdin)
except json.JSONDecodeError:
sys.exit(0) # 入力が読めないときは邪魔をしない
tool_input = data.get("tool_input") or {}
file_path = tool_input.get("file_path") or tool_input.get("notebook_path") or ""
if not file_path:
sys.exit(0)
if is_protected(file_path):
tool = data.get("tool_name", "unknown")
print(
f"BLOCKED: {tool} access to secret file '{os.path.basename(file_path)}' "
"is not allowed. Secret files (.env*) must never be read or modified by AI. "
"Ask the user to handle secrets manually.",
file=sys.stderr,
)
sys.exit(2) # exit 2 = ツール実行をブロックし、stderr が Claude に返る
sys.exit(0)
if __name__ == "__main__":
main()
実測では、.env 系への Read / Write(.env.backup の新規作成を含む)がすべてこのフックで止まりました。
YOLO モードでも同じです。
対照用に置いた config.txt は素通りしたので、誤爆もありません。
ブロック時に Claude が受け取るメッセージはこうなります(ログ抜粋)。
PreToolUse:Read hook error: [python3 "$CLAUDE_PROJECT_DIR"/.claude/hooks/guard_env.py]:
BLOCKED: Read access to secret file '.env' is not allowed. Secret files (.env*)
must never be read or modified by AI. Ask the user to handle secrets manually.
このメッセージを受けた Claude の振る舞いがよくできていて、「以下のファイルの内容を手動で確認して共有していただく必要があります」と cat .env のコマンド例を私に提示して、自分は手を引きました。
なぜブロックされたか・代わりにどうするかをこちらで書けるので、Claude が次の行動を選びやすくなります。
2方式の違い
両方を動かして分かった違いを表にまとめます。
| 観点 | permissions.deny | PreToolUse フック |
|---|---|---|
| 導入の手間 | settings.json に4行 | 設定+スクリプト約40行 |
| ブロック時のメッセージ | 汎用文言で固定 | 自作の文言を Claude に渡せる |
| カバー範囲 | Bash の cat 等も塞ぐ(実測) |
matcher に書いたツールのみ(Bash 経由は未検証) |
メッセージの差は、実際に見比べると印象がだいぶ違います。
deny の文言はファイル単位のルールで弾いても File is in a directory that is denied と出るため、初見では原因が分かりにくい。
実測でも Claude は権限設定の変更を提案しがちでした。
フック側は前述のとおり、理由と代替行動まで伝えられます。
もうひとつの差は柔軟性です。
フックはロジックなので、.env.example や .env.sample のようなテンプレートだけ許可する、といった例外が数行で書けます。
deny ルールはパターンの列挙なので、この種の例外表現は苦手です。
そして列挙には漏れが出ます。
これは私自身の設定で確かめられました。
~/.claude/settings.json には以前から .env .env.local .env.development への ask ルールと .env.production への deny ルールを入れていて、実務でこの網に助けられてきた側です。
ただ今回の検証で .env.staging を置いてみたところ、どのパターンにも掛からず素通りしました。
実務で staging 系のファイルを扱う前に、検証で気づけたのは幸運でした。
列挙式の防御は、列挙した分しか守りません。
フックの .env.* を basename で判定する方式なら、staging も backup も、来年増える謎のサフィックスもまとめて塞げます。
settings.json の権限設定そのものについては、以前書いたこちらの記事も参考になるはずです。
残る限界
一方で、どちらの方式にも共通の限界があります。
公式ドキュメントの警告をそのまま引用します。
Read and Edit deny rules apply to Claude's built-in file tools and to file commands Claude Code recognizes in Bash, such as
cat,head,tail, andsed. They don't apply to arbitrary subprocesses that read or write files indirectly, like a Python or Node script that opens files itself. For OS-level enforcement that blocks all processes from accessing a path, enable the sandbox.
つまり python -c "print(open('.env').read())" のような、スクリプトが自力でファイルを開くケースは deny ルールの守備範囲外です。
OS レベルで全プロセスから封じたい場合はサンドボックス機能を使うことになります。
フック方式にも同種の穴があります。
matcher に Read|Edit|Write と書いた以上、Bash ツール経由の読み取りはそもそも検査対象になりません。
Bash 側は今回実測していないので断定はしませんが、構造上は素通りするはずです。
deny が cat を塞いだ実測結果を踏まえると、ここは deny に受け持たせるのが素直です。
なお、サブエージェント経由のツール呼び出しにガードが効くかも今回は検証していません。
まとめ
.env を守る仕組みは、permissions.deny の4行でもフックの40行でも実測で機能しました。
私が併用を選んだのは、deny が cat 経由まで塞ぐ土台になり、フックが例外と誘導メッセージを受け持つ——役割が重ならないからです。
検証で自分の設定の列挙漏れに気づいた身としては、片方に全部を任せる気になれません。
Xをフォローいただけると嬉しいです!
AI駆動開発(特に Claude Code)のノウハウや Tips をよく発信しています!

