株式会社TIMEWELLの濱本隆太です。今日はClaude Codeを業務に組み込んでいる方向けに、見落とされがちなシークレット漏洩のリスクと、その止め方をお話しします。
Claude Code(Anthropic社が提供する公式コーディングエージェント)を毎日のパートナーとして使う方が、社内外で一気に増えてきました。私たちTIMEWELLでも、ほぼ全プロジェクトで標準ツールとして組み込んでいます。
その流れと並行して、ここ数週間で立て続けに相談が来ているのが、こういう話です。
Claude Codeは、あなたのプロジェクトを開いた瞬間に
.envを読み込んでしまう可能性がある
.env の中身は、Stripeの本番トークン、AWSのアクセスキー、OpenAIのAPIキー、Anthropic自身の sk-ant- で始まるキーなど、外に出せない情報の集まりです。Claude Codeはそれらをコンテキストとしてメモリに乗せ、Anthropicのサーバーに送信されるログにも混入する可能性があります。一度サーバーログに乗ったシークレットは、事実上「漏洩した」のと同じ扱い。あなたが社内Slackで謝って収まる話ではありません。
先日、海外の開発者 @darkzodchi 氏が公開した .env 保護ガイドが英語圏で話題になりました。本稿は、その内容を踏まえつつ、日本の開発現場で本当に必要な対策だけを順序立ててまとめ直したものです。
結論を先に言うと、守るべき鍵は1つ。settings.json に書く数行の deny ルールです。ただ、それだけでは穴が残ります。漏洩経路は3つあって、それぞれに別の対策が要ります。最後まで読めば、「Claude Codeがどんな賢い推論をしても、物理的にシークレットに触れない」状態が30分で作れます。
守るべきは「3つの漏洩経路」
本題に入る前に、全体像です。「.gitignore に .env を入れた」「CLAUDE.mdで『.envを読むな』と書いた」で安心している方が多いのですが、それでは1つの経路しか塞げていません。
| 経路 | 内容 | 主な対策 |
|---|---|---|
| ① 直接読み取り | Claudeが .env を Read ツールで開く |
settings.json の deny ルール |
| ② 実行時の出力 | テストやアプリ起動時のログ・エラーにシークレットが出る | .env.test のダミー値運用 |
| ③ grep / search の巻き添え | 検索結果にシークレット行が含まれる | deny ルール + コードレビュー |
これら3つに加え、究極の保険として、
- pre-commit hookによる自動検出
- コンテナ分離による物理的遮断
の2段構えを用意するのが本稿のゴールです。
「うちは個人開発だから大丈夫」「社内ツールだから漏れても大したことない」と思った方こそ、続きを読んでください。シークレット漏洩は、いつも「そんなつもりじゃなかった」という文脈で起きます。
なぜ CLAUDE.md では守れないのか
最初に、よくある誤解を解いておきます。
Claude Codeには CLAUDE.md というプロジェクト固有の指示書を置く仕組みがあります。多くの方はここに、
## セキュリティルール
- .envファイルを絶対に読み込まないでください
- シークレット情報を出力してはいけません
と書いて「これでClaudeはルールを守ってくれる」と安心しています。
しかし、CLAUDE.md は "お願い" にすぎません。システムレベルの強制力は持たず、Claudeの判断で従うかどうかが決まる「ソフトな指示」です。
CLAUDE.md が守られない具体的なシチュエーション
私たちのチームが実際に遭遇した、あるいは再現した「ClaudeがCLAUDE.mdを無視するケース」を挙げます。
- 長いコンテキストで埋もれる:複雑なタスクを何十ターンも続けると、初期に読み込んだCLAUDE.mdの内容がモデルの「注意」から薄れていきます。判断力が落ちた瞬間に
.envを読みに行ってしまう。 - タスクが曖昧で「解決のヒント」を求めに行く:「環境変数が効いていないみたい」「なぜか認証が通らない」というデバッグ文脈で、Claudeが原因調査のために
.envを開く。善意ですが、結果的にシークレットが会話に乗ります。 - エラーメッセージに釣られる:テスト実行で
Cannot find env variable FOOが出ると、Claudeは「.envを確認しよう」と動きがちです。 - エージェントSDKやサブエージェント経由の実行:CLAUDE.mdはメインのエージェントには読まれても、サブエージェントやツール経由の実行までは伝播しないことがあります。
2026年4月にはGitHub上で「CLAUDE.mdで禁止していたのにClaudeが .env の中身を会話にechoした」という報告が上がり、議論を呼びました。
唯一の確実な防御は「システムレベルの deny」
settings.json の permissions.deny ルールは、Claude Codeの実行エンジン側で強制される物理的な制約です。
- CLAUDE.md:「この部屋に入らないでください、お願いします」と書いた張り紙
- settings.jsonのdeny:そもそもドアノブに手が届かない鍵付きの部屋
この差は決定的です。これから紹介するのは、その「鍵」の掛け方です。
Looking for AI training and consulting?
Learn about WARP training programs and consulting services in our materials.
漏洩経路① Claudeが.envを直接読むパターン
最も分かりやすく、最も対策がしやすいのがこの経路です。
何が起こるか
プロジェクトを開いたClaude Codeは、構造を把握するためにファイルツリーをスキャンします。多くの場合、この段階では .env の中身までは読みません。状況が変わるのは、あなたが次のような指示を出した瞬間です。
- 「環境変数が正しく読み込まれているか確認して」
- 「Stripeの連携部分を見て、設定ミスがないか教えて」
- 「このプロジェクトの設定周りをレビューして」
Claudeは「なるほど、.env を見ればよいのだな」と判断し、Read ツールで .env を開きます。すると STRIPE_SECRET_KEY=sk_live_... OPENAI_API_KEY=sk-... がそのまま会話コンテキストに乗ります。
コンテキストに乗るとは、
- Claudeの推論に使われる
- あなたの画面に表示される
- Anthropicのサーバーに送信されるログに含まれる可能性がある
- あとから
/resumeなどでセッションを再開したときにも残る
ということです。
対策:settings.jsonのdenyルール
~/.claude/settings.json(グローバル)またはプロジェクト配下の .claude/settings.json に、以下を追加します。
{
"permissions": {
"deny": [
"Read(**/.env*)",
"Read(**/.dev.vars*)",
"Read(**/*.pem)",
"Read(**/*.key)",
"Read(**/secrets/**)",
"Read(**/credentials/**)",
"Read(**/.aws/**)",
"Read(**/.ssh/**)",
"Read(**/config/database.yml)",
"Read(**/config/credentials.json)",
"Read(**/.npmrc)",
"Read(**/.pypirc)",
"Write(**/.env*)",
"Write(**/secrets/**)",
"Write(**/.ssh/**)"
]
}
}
ポイントを整理します。
**/というグロブは、プロジェクトの任意のサブディレクトリを意味します。apps/web/.envやpackages/api/.env.productionも確実にブロックできます。.env*と書くのは、.env.local、.env.production、.env.test、.env.stagingなど派生ファイルを一網打尽にするため。Writeも塞ぐのは、Claudeが「親切心で」.envを書き換えてしまう事故を防ぐため。ある日突然キーが消えている、という事態を未然に止められます。.pemや.keyはSSL証明書とSSH秘密鍵。これらが読まれると認証インフラ全体が危険にさらされます。.npmrcと.pypircにはnpmやPyPIのトークンが入ることがあります。社内プライベートレジストリを使う組織では特に重要。
試してみる
設定を保存したらClaude Codeを再起動し、
.envファイルの中身を見せてください。
と頼んでみてください。Claudeは「申し訳ありません。このファイルへのアクセスは設定により拒否されています(Read denied by permissions)」と返します。これが「物理的に触れない」状態です。最も単純で効果の高い対策はここで完了です。
漏洩経路② コマンド実行の出力から漏れるパターン
ここからが本題です。deny ルールだけでは、シークレットは漏れます。多くの方が気づいていない、最も危険な経路がこれです。
具体例:テスト実行でシークレットが出力される
Claude Codeに「テストを実行して」と頼むと、Bash ツール経由で npm test や pytest が走ります。たとえば次のようなテストがあったとします。
// tests/stripe.test.ts
test("Stripe API に接続できる", async () => {
const res = await fetch("https://api.stripe.com/v1/charges", {
headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}` },
});
expect(res.status).toBe(200);
});
ネットワーク障害でテストが失敗すると、アサーションライブラリやフレームワークが気を利かせて、リクエストの詳細をコンソールにダンプすることがあります。
FAIL tests/stripe.test.ts
Expected 200, received undefined
Request: GET https://api.stripe.com/v1/charges
Headers: { Authorization: "Bearer sk_live_51H...abcDEF" }
Error: ETIMEDOUT
この瞬間、Claude Codeの Bash ツールはこの出力を丸ごと会話コンテキストにキャプチャします。Claudeは .env を一切開いていません。denyルールは何も破られていません。それでもシークレットは漏れた、という形です。
他にもある「実行時漏洩」の典型例
- DB接続エラー:タイムアウト時に
psql: connection to server at "db.example.com", user "admin", password "real_password_here" failedのように接続文字列が丸出しになる - サードパーティSDKのデバッグログ:
DEBUG=*環境変数が有効なとき、多くのSDKが認証ヘッダー付きのリクエスト詳細を吐く - スタックトレース:Pythonの一部ライブラリは例外時に引数をそのまま表示するため、
api_key=sk-...がスタックトレースに混入する
対策:.env.test と「ダミー値運用」
テストやローカル起動時に使うシークレットは、本物と分けて管理するのが正攻法です。
# .env.test ── コミットOK、漏れてもOKの値のみ
STRIPE_SECRET_KEY=sk_test_not_a_real_key_dummy_value
DATABASE_URL=postgres://test:test@localhost:5432/testdb
OPENAI_API_KEY=sk-test-dummy-key-for-mocking
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AKIAIOSFODNN7EXAMPLE などはAWS公式ドキュメントが「例示用」として明示しているダミー値です。万が一コミットされても、流出してもゼロダメージ。
テストフレームワーク側は .env.test を優先的に読むよう構成します。
Node.js(JestやVitest)の場合は次のように書きます。
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { loadEnv } from "vite";
export default defineConfig(({ mode }) => ({
test: {
env: loadEnv("test", process.cwd(), ""),
},
}));
Python(pytest)の場合はこうなります。
# conftest.py
import os
from dotenv import load_dotenv
def pytest_configure():
load_dotenv(".env.test", override=True)
こうすればClaude Codeが「テストを走らせて」と動いてもダミー値しか使われません。ログに残るのは sk_test_not_a_real_key_dummy_value だけ。漏れて困るものがそもそも存在しなくなります。
追加のテクニック:出力フィルタ
本番環境変数でどうしてもローカル検証が必要な場合は、自前の出力マスクを噛ませる方法もあります。次のようなラッパースクリプトでコマンド出力を削っておく形です。
#!/bin/bash
# scripts/safe-run.sh
"$@" 2>&1 | sed -E \
-e 's/sk-ant-[A-Za-z0-9_-]+/sk-ant-***REDACTED***/g' \
-e 's/sk_live_[A-Za-z0-9]+/sk_live_***REDACTED***/g' \
-e 's/AKIA[0-9A-Z]{16}/AKIA***REDACTED***/g'
これを介してコマンドを実行するようClaudeに伝えておけば、万一の時もログは守られます。
漏洩経路③ grepやsearchから漏れるパターン
3つ目の経路は地味ですが意外と頻発します。
何が起こるか
Claudeはコードベース内で関数や変数を探すために Grep や Glob ツールを使います。
getUserById関数がどこで定義されているか探して
と頼むと、Claudeは grep -rn "getUserById" 相当を走らせます。このとき、もしどこかの設定ファイルやコメントに次のような行があったら:
// TODO: 旧APIキー sk_live_oldkey_abc123 を getUserById に渡す実装を削除する
function getUserById(id) { ... }
grepの出力にはマッチした周辺行がそのまま乗り、シークレットが会話に流れます。Claudeは .env を開いていないし、テストも実行していない。ただコードを検索しただけです。
対策1:denyルールの徹底
secrets/、credentials/、.aws/、.ssh/ を deny に入れるのは、この経路を塞ぐのにも有効です。grepが到達できない場所には結果も含まれません。
対策2:コメント・ログからシークレットを一掃する
地道な作業ですが、本来やるべきハウスキープです。
- 古いシークレットがコメントやTODOに残っていないか、定期的にgrepで監査する
- SlackチャンネルURL、Webhook URL、内部APIのベアラートークンも対象
- 後述のpre-commit hookで、コミット時点でブロックする
対策3:機密っぽいディレクトリを丸ごと隠す
プロジェクト内に「機密っぽいディレクトリ」がある場合、明示的にClaudeから見えないようにします。現時点ではdenyルールで代用する形ですが、特に大きめのリポジトリでは logs/、dumps/、backup/ も deny に入れておくと事故率が目に見えて下がります。
"deny": [
"Read(**/logs/**)",
"Read(**/dumps/**)",
"Read(**/backup/**)"
]
完全版settings.json:実戦で使えるコピペ設定
ここまでの内容を踏まえた、TIMEWELLで実際に使っているベース設定を共有します。~/.claude/settings.json に貼り付けて、チーム全員で運用してください。
{
"permissions": {
"allow": [
"Read",
"Glob",
"Grep",
"LS",
"Edit",
"MultiEdit",
"Write(src/**)",
"Write(tests/**)",
"Write(docs/**)",
"Bash(npm run *)",
"Bash(npm test *)",
"Bash(npx tsc *)",
"Bash(npx vitest *)",
"Bash(git status)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(git add *)",
"Bash(git commit *)"
],
"deny": [
"Read(**/.env*)",
"Read(**/.dev.vars*)",
"Read(**/*.pem)",
"Read(**/*.key)",
"Read(**/*.p12)",
"Read(**/*.pfx)",
"Read(**/secrets/**)",
"Read(**/credentials/**)",
"Read(**/.aws/**)",
"Read(**/.ssh/**)",
"Read(**/.gcp/**)",
"Read(**/.azure/**)",
"Read(**/config/database.yml)",
"Read(**/config/credentials.json)",
"Read(**/config/master.key)",
"Read(**/.npmrc)",
"Read(**/.pypirc)",
"Read(**/logs/**)",
"Read(**/dumps/**)",
"Write(**/.env*)",
"Write(**/secrets/**)",
"Write(**/.ssh/**)",
"Write(.github/workflows/*)",
"Bash(rm -rf *)",
"Bash(sudo *)",
"Bash(git push *)",
"Bash(git push --force *)",
"Bash(npm publish *)",
"Bash(curl * | sh)",
"Bash(curl * | bash)",
"Bash(wget *)",
"Bash(chmod 777 *)"
],
"defaultMode": "acceptEdits"
}
}
ポイント解説
allowは「作業で本当に使うもの」だけ:ホワイトリスト方式のほうが事故が減りますWrite(src/**)のようにディレクトリを絞る:Claudeが/etcや~/.sshに書き込む事故を防ぎますBash(git push *)をdenyに:リモートへのpushは人間の判断を挟みたいチーム向け。自動pushしたければallow側に移動Bash(curl * | sh)をdenyに:インターネットから拾ってきたスクリプトを即実行させない、最も基本の防御defaultMode: acceptEdits:私たちは「編集は許可、ファイル作成やコマンド実行は確認」というバランスで運用しています
プロジェクト固有のルールがあれば .claude/settings.json でオーバーライドできます。「このプロジェクトではTerraformを触るので Bash(terraform *) を allow する」といった調整をプロジェクト単位で重ねるのがおすすめです。
pre-commit hookで二重の防御
Claude Code側で対策しても、人間が手作業で .env をコミットしてしまう事故は別問題です。これを機械的に防ぐため、Gitのpre-commit hookを仕込みます。
.git/hooks/pre-commit に以下を保存し、chmod +x .git/hooks/pre-commit で実行権限を付けてください。
#!/bin/bash
# 秘密情報を含むコミットをブロックする pre-commit フック
PATTERNS=(
'sk-ant-' # Anthropic API キー
'sk-live-' # Stripe ライブキー
'sk_live_' # Stripe ライブキー(別表記)
'ghp_' # GitHub Personal Access Token
'gho_' # GitHub OAuth Token
'ghs_' # GitHub App Server Token
'AKIA[0-9A-Z]{16}' # AWS Access Key ID
'xox[bpors]-' # Slack Token
'SG\.[A-Za-z0-9_-]{22}' # SendGrid API Key
'eyJ[A-Za-z0-9_-]{20,}' # JWT
'BEGIN[[:space:]]\+\(RSA\|DSA\|EC\|OPENSSH\|PGP\)\?[[:space:]]*PRIVATE KEY'
)
BLOCKED_FILES=('.env' '.env.local' '.env.production' 'credentials.json' 'id_rsa' 'id_ed25519')
# パターンマッチ
for pattern in "${PATTERNS[@]}"; do
if git diff --cached --diff-filter=ACM | grep -qE "$pattern"; then
echo "BLOCKED: コミットにシークレットらしきパターン '$pattern' が含まれています。"
echo " git diff --cached で確認し、該当行を削除してから再度コミットしてください。"
exit 1
fi
done
# ファイル名チェック
for file in "${BLOCKED_FILES[@]}"; do
if git diff --cached --name-only | grep -qF "$file"; then
echo "BLOCKED: 機密ファイル '$file' がステージに含まれています。"
echo " git reset HEAD -- $file を実行してください。"
exit 1
fi
done
# 拡張子チェック
if git diff --cached --name-only | grep -qE '\.(pem|key|p12|pfx)$'; then
echo "BLOCKED: 鍵ファイル(.pem/.key/.p12/.pfx)がステージに含まれています。"
exit 1
fi
echo "pre-commit セキュリティチェックに合格しました。"
exit 0
なぜパターンベースの検査が必要か
「.env を .gitignore に入れてあるから大丈夫」というのは幻想です。次のようなケースで漏れます。
- コメント内に一時的に貼り付けたトークンを消し忘れる
- ドキュメントの例示に「ついつい本物」を書いてしまう
- Jupyter Notebookのセル出力にAPIレスポンスが残る
- デバッグ用の
console.log(process.env)を消し忘れる
pre-commit hookは、これらのうっかりを出口で止める最後の砦です。
より本格的にやるなら gitleaks や trufflehog
本記事の簡易フックで足りない場合は、gitleaks や trufflehog の導入を検討してください。数百種類のシークレットパターンを網羅していて、CIに組み込めばPR単位で検出できます。
TIMEWELLでは、ローカルのpre-commitに本記事のフック、CIにgitleaks、という2段構えを標準にしています。
コンテナ分離という「核オプション」
ここまでの対策で、ほとんどのプロジェクトは十分です。ただし、クライアント案件の本番クレデンシャルを扱う場合や、医療・金融といった高規制業界では、もう一段階の防御が要ります。
それが「コンテナ分離」です。
発想
Claude Codeを、.env が物理的に存在しない仮想環境の中で動かす、という発想です。
# Claude Code 用の Docker コンテナを起動する例
docker run -it \
-v "$(pwd)":/app \
-v /dev/null:/app/.env:ro \
-v /dev/null:/app/.env.local:ro \
-v /dev/null:/app/.env.production:ro \
--env-file /dev/null \
claude-code-dev
ポイントは -v /dev/null:/app/.env:ro の行です。コンテナ内の /app/.env を空ファイル(/dev/null)で上書きマウントするトリックで、Claude Codeから見ると .env は「中身のない空ファイル」になります。
運用設計
- 開発時:
.envを使わず、コンテナ起動時に-e KEY=valueで必要な環境変数を直接渡す。あるいはVaultや1Password CLI経由で一時的に注入する - 本番ビルド:CI/CDのシークレットストレージ(GitHub Actions Secrets、AWS Secrets Managerなど)を使い、ソースコードに一切シークレットを含めない
- ローカル機密:
.envは~/.env-vault/のようなプロジェクト外ディレクトリに退避し、リポジトリフォルダには置かない
やりすぎに見えますが、一度やってしまえば「事故り得ない」設計です。TIMEWELLでも、本番DBに触れる可能性のある案件ではこの構成を標準にしています。
今日やるべき6つのチェックリスト
長くなったので、アクションだけまとめます。次のClaude Codeセッションを始める前に、以下を確認してください。
- 1.
~/.claude/settings.jsonに.env*やsecrets/、.ssh/のdenyルールが書かれているか - 2. テストは
.env.test(ダミー値)を読むよう設定されているか。本物のシークレットがテスト経由でログに出ない状態か - 3.
.git/hooks/pre-commitにシークレットパターン検出フックが仕込まれているか。実行権限(chmod +x)はあるか - 4. 本番クレデンシャルは平文ファイルではなく、1PasswordやAWS Secrets Manager、HashiCorp Vaultなどに保管されているか
- 5.
.env*がすべて.gitignoreに含まれているか。履歴にコミット済みのシークレットが残っていないか(git log -p | grep -E 'sk_live_|sk-ant-'で確認) - 6.
.envは極力プロジェクトディレクトリ外に置く、あるいはコンテナでマウント除外しているか
6つすべてにチェックが付けば、あなたの環境は「Claude Codeが何をしても、シークレットには触れられない」状態に限りなく近づきます。
ゼロなら、次のあいまいなプロンプト一つで、あなたのAPIキーがAnthropicのサーバーログに記録されます。明日やる、ではなく今この瞬間に手を動かしてください。
AIセキュリティを「組織の標準」にしたいなら
ここまで紹介してきたのは、開発者個人がローカル環境を守るための対策です。ただ、AIエージェントを業務に本格導入する企業ほど、この話は個人の自衛では収まりません。
- 全社員が同じレベルでsettings.jsonを書けているか
- 業務委託・外部パートナーが触るリポジトリでも同じ設定が強制されているか
- インシデントが起きた時、誰がどう検知して、どこで止めるのか
- 監査・契約上の説明責任を、どの粒度で果たせばよいか
このあたりは、ガイドラインを社内に配るだけでは動きません。実務に落とすには、現場のワークフロー、契約、教育、モニタリングまで一気通貫で設計する必要があります。
TIMEWELLのWARPコンサルティングは、まさにこの「AIを安全に、かつ最大効率で組織に組み込む」ための伴走支援を提供しています。Claude CodeをはじめとするAIエージェントの社内ロールアウト設計、シークレット管理ポリシー、開発者教育、インシデント対応のプレイブック作成までを、月次の伴走形式で進めます。
「AIを使い倒したいが、セキュリティが怖くて踏み込めない」「現場には任せているが、本当に守れているか分からない」「経営層に対して、安全性を説明できる材料が欲しい」。そんな段階の方は、一度ご相談ください。30分のオンライン相談から、御社の現状に合わせた次の一手をお話しします。
AIセキュリティは、自己流で組み上げるより、組織として「標準を持つ」ことのほうが圧倒的に効きます。WARPはその標準づくりの伴走役です。
まとめ
AIエージェントに開発を任せる時代になり、私たちは新しい種類のリスクと向き合うことになりました。これまでの「人間だけが触るコード」の時代では、「信頼できるメンバーだけがローカルで .env を読む」という運用で十分でした。Claude Codeは、あなたのプロジェクトを開いた瞬間からファイルを読みに行く「無数の新人エンジニア」のような存在です。
新人に「機密資料のキャビネットは開けないで」と張り紙をするのがCLAUDE.md。
鍵を物理的に渡さないのがsettings.jsonのdenyルール。
万一開けてもダミーしか入っていないのが .env.test 運用。
そして、機密資料はそもそも別のビルに保管するのがコンテナ分離。
この4層を重ねて、はじめて安心してAIエージェントに開発を任せられます。
最後に一言だけ。セキュリティは「一度やって終わり」ではありません。新しいシークレット形式が登場するたびにpre-commitフックは更新が必要ですし、Claude Codeのバージョンアップで settings.json のスキーマが変わる可能性もあります。本記事は2026年5月時点のベストプラクティスとしてまとめましたが、定期的な見直しを強くおすすめします。
あなたの開発体験が、より速く、より安全になりますように。
株式会社TIMEWELL 濱本隆太
参考文献・情報源
- Anthropic公式ドキュメント "Claude Code settings"(https://docs.anthropic.com/)
- darkzodchi (@zodchiii), "The .env Setup That Keeps Claude Code From Leaking Your Secrets"(2026年4月)
- gitleaks 公式リポジトリ(https://github.com/gitleaks/gitleaks)
- OWASP Secrets Management Cheat Sheet
- AWS 公式ドキュメント "Example AWS Access Keys"(ダミー値の出典)
