
Windows の Claude Code で .env を守る ― deny ルールはどこまで効くのか
はじめに
Claude Code を使い始めて3か月ほどになります。日々勉強中なのですが、先日 deny ルールについて調べていて、自分の理解が足りていなかった事に気付きました。
.env 等を読ませたくない場合は deny ルールを追加するのですが、実際にどこまで効くのか、いくつか試したところ、Read(.env*) で塞げるパターンは限られているという事が分かったので共有します。
検証環境
- Claude Code 2.1.278
- Windows 11(ネイティブ)、WSL2(Ubuntu 24.04)
- ダミー文字列を入れた
.envだけを置いた検証用ディレクトリ
以下の結果は、すべてこの環境で実際に試したものです。
deny ルール
例えば .env を読込禁止にしたい場合、settings.json に deny ルールを書きます。
{
"permissions": {
"deny": [
"Read(.env*)"
]
}
}この状態で .env を読ませようとすると、実際にブロックされます。
しかし、このルールでは防げないパターンが存在しました。
何が防げて、何が読めてしまうのか
以下、Read(.env*) を deny に入れた状態で、いくつかのコマンドを試した結果です。
| ツール | コマンド | 結果 |
|---|---|---|
| Read | — | ⛔ ブロック |
| Bash | cat .env | ⛔ ブロック |
| Bash | grep SECRET_KEY .env | ⛔ ブロック |
| PowerShell | Get-Content .env | ⛔ ブロック |
| PowerShell | Select-String SECRET_KEY .env | ⛔ ブロック |
| Bash | grep -r SECRET_KEY . | ⚠️ 読めた |
| Bash | python -c "print(open('.env').read())" | ⚠️ 読めた |
| PowerShell | Get-ChildItem -Force -File| | ⚠️ 読めた |
| PowerShell | Get-Content (Join-Path . '.env') | ⚠️ 読めた |
| PowerShell | [IO.File]::ReadAllText('...\.env') | ⚠️ 読めた |
⚠️ の行では、実際に .env の中身が出力されました。
cat .env や grep SECRET_KEY .env が止まるのは、Read のルールが Bash 内のファイルコマンドまで見て判定をしているからのようです(権限を設定する – Read と Edit)。
cat "$(pwd)/.env" のようにパスの書き方を変えてもブロックされました。
ただし、このルールが適用されないパターンがありました。(後述)
意外だったのは、Read の deny ルールが Bash だけでなく PowerShell にも効くことです。
PowerShell は別枠だと思い込んでいたので、ここは認識を改めました。
PowerShell 用のルールを別に書かなくても、主要なファイルコマンドは塞がるようです。
なぜ読めてしまうのか
ブロック可能な条件は「Claude Code が認識するコマンドで、引数から読み取り先のパスを特定できること」で、以下のようなコマンドでは deny ルールが適用されず、.env が読めてしまいます。
- ファイル名を書かない(
grep -r pattern .、Get-ChildItem | Select-String pattern) - パスを式や変数から組み立てて、Claude Code がファイル名を読み取れない形(
cat $(echo .env)、Get-Content (Join-Path . '.env')) - Python や .NET の API が内部でファイルを開く(
[IO.File]::ReadAllText)
注意したいのは、コマンド文字列に .env が残っていてもブロックされない点です。
同じファイルを指していても、書き方によって結果が変わりました。
| ツール | コマンド | 結果 |
|---|---|---|
| Bash | cat "$(pwd)/.env" | ⛔ ブロック |
| Bash | cat $(echo .env) | ⚠️ 読めた |
| PowerShell | Get-Content './.env' | ⛔ ブロック |
| PowerShell | Get-Content "$PWD\.env" | ⚠️ 読めた |
4つとも .env という文字列はコマンドに含まれていますが、Bash はコマンド置換を挟んでも、引数にファイル名がそのまま残っていればブロックされました("$(pwd)/.env" は残り、$(echo .env) は残らない)。
PowerShell は引数が式や変数展開になった時点で、文字列に .env が見えていても対象外になるようです。
ドキュメントにも、Read と Edit のルールはファイルを名前で指定せずに読み取るコマンドや、スクリプトが自分でファイルを開くようなサブプロセスには適用されないと明記されています。
また、パスへのアクセスを OS レベルで止めるなら、 sandbox を使うよう案内されています。
Windows(ネイティブ) で対応する場合
Windows の Claude Code には Bash と PowerShell という二つのシェルツールがあり、Git Bash がインストールされていても PowerShell が既定で有効で、Claude はコマンド毎にどちらを使うか選びます。
前述のとおり Read(.env*) 自体は PowerShell にも効くので、追加で以下の2つの対応が必要でした。
1. Bash から powershell.exe を呼ぶ経路
これが Windows 固有の穴です。Bash ツールの呼び出しなので PowerShell(...) は参照されません。
Read は認識できるファイルコマンドとその引数ですが、powershell.exe はその対象外なので、コマンドに .env と書かれていても判定されません。
| deny の内容 | コマンド | 結果 |
|---|---|---|
Read(.env*) | powershell.exe -NoProfile -Command “Get-Content .env” | ⚠️ 読めた |
Read(.env*), Bash(powershell*), Bash(pwsh*) | 同上 | ⛔ ブロック |
| 同上 | /c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -NoProfile -Command “Get-Content .env” | ⚠️ 読めた |
Bash(powershell*) と Bash(pwsh*) を足せば塞がりますが、フルパスで呼ばれると効きません。ドキュメントにも Bash(curl *) が /usr/bin/curl を止められない例が挙がっています。
2. フックで検査する
deny ルールで防げない範囲は、PreToolUse フックで拾います。
フックはコマンド全体の文字列を受け取るので、Read ルールが取りこぼした「パスを組み立てる形」も、.env という文字列が残っていれば拾えます。
注意点は、フックも deny ルールと同じくツールごとに指定する事です。
matcher に "Bash" とだけ書くと、PowerShell の呼び出しでは発火しません。両方を見るには "Bash|PowerShell" と書きます。
Windows(ネイティブ) 向けの設定
{
"permissions": {
"deny": [
"Read(.env*)",
"Bash(powershell*)",
"Bash(pwsh*)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|PowerShell",
"hooks": [
{
"type": "command",
"command": "python \"${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-env.py\""
}
]
}
]
}
}フックで実行している検査スクリプト例
以下、Claude Codeで生成した、先述の表の ⚠️ を塞ぐ最小構成の例です。
しかし、様々なコマンドのパターンがある以上、全てを網羅するのは現実的でないと感じました。
#!/usr/bin/env python3
import json
import re
import sys
DENY = [
(r"\.env",
".env という名前がコマンドに現れています"),
(r"\bgrep\b[^|;&]*(-[a-z]*r|--recursive)",
"ファイル名を指定せずに再帰的に読んでいます"),
(r"\b(get-childitem|gci|ls|dir)\b.*\|.*\b(select-string|sls)\b",
"ファイル一覧をそのまま読み取りコマンドに渡しています"),
]
def decide(command):
for pattern, reason in DENY:
if re.search(pattern, command, re.IGNORECASE):
return reason
return None
def main():
try:
payload = json.load(sys.stdin)
except Exception:
return 0
if payload.get("tool_name") not in ("Bash", "PowerShell"):
return 0
command = payload.get("tool_input", {}).get("command") or ""
reason = decide(command)
if reason is None:
return 0
try:
sys.stderr.reconfigure(encoding="utf-8")
except Exception:
pass
sys.stderr.write(f"[guard-env] 拒否しました: {reason}\nコマンド: {command}\n")
return 2
if __name__ == "__main__":
sys.exit(main())Linux(WSL2)で対応する場合
Linux でも Read(.env*) だけでは、Windowsと同様に対応しきれないパターンが発生します。
Bash(...) でコマンド文字列を塞ぐ手はありますが、漏れが発生する可能性があります。
その為、Linux で確実なのは sandbox です。
OS レベルでシェルツールとその子プロセスのファイルアクセスを制限する為、コマンドの書き方に関係なく防ぐ事ができます。
Linux(WSL2) 向けの設定
{
"permissions": {
"deny": ["Read(.env*)"]
},
"sandbox": {
"enabled": true,
"allowUnsandboxedCommands": false
}
}allowUnsandboxedCommands を false にしているのは、ブロックされたコマンドを Claude が sandbox の外で再実行できないようにする為です。
また、組み込みの Read ツールは sandbox の管轄外なので、Read(.env*) は引き続き必要です。
対応方法まとめ
上から順に有効な手段となります。
1. 実値を .env に置かない
そもそも置かない事が一番有効です。
開発用の .env はダミーやローカル DB の情報だけにして、実値は各クラウドサービスの Secrets Manager や CI の変数に置けば、読まれても実害がありません。
2. sandbox を有効にする
OS レベルで Bash・PowerShell ツールとその子プロセスのファイルアクセスを制限できます。
前述のとおり、コマンドの書き方に関係なく効きますが、組み込みの Read ツールは管轄外なので、deny ルールとの併用が必要です。
ただし、ネックはWindows で直接動かしていると使えないことです。
対応は macOS、Linux、WSL2 のみで、WSL 1 も対象外です。
Windows 専用ツールを使っていないプロジェクトなら、移すのがオススメです。
3. フックで検査する
Bash|PowerShell を matcher にした PreToolUse フックで、コマンド文字列を検査してブロックします。
しかし、抜け道を一つずつ塞ぐのは、正直きりがないと感じました。
4. Read(.env*) を書く
組み込みツールと、Bash・PowerShell 双方の主要なファイルコマンドを防げます。
ただ、これだけでは守り切れません。
5. 承認プロンプトの目視確認
承認プロンプトを読むのも対策のひとつです。ただ目視で確認するのは限界がある為、機械的に弾けるものはフックや sandbox に任せるのが現実的だと思います。
おわりに
deny ルールはClaude Codeがうっかり読んでしまう事を防ぐ層としては有効です。
ただ一番確実なのは、読まれて困るものをそもそも置かないことでした。
とはいえ、Claude Code が生産性を大幅に引き上げてくれる非常に強力なツールであることは間違いありません。だからこそ、利用方法をしっかり学んだ上で、日々の業務に活用していきたいと思います。