Claude Codeが.envをBash環境へ読み込んでいた問題 — 原因とv2.0.64の修正
Claude CodeがプロジェクトのBash実行環境へ.envを自動読み込みしていた既知の問題の原因、v2.0.64での修正、今も有効な3層の対策をまとめます。
Claude Codeには、プロジェクトの.envファイルをBashツールの実行環境へ気づかないまま読み込んでしまう不具合がありました。LaravelやRailsのテストスイートが開発用データベースを誤操作し、データが失われた実害も報告されています。.envに秘密情報を置いている場合は、意図しない相手にその値が渡る経路にもなります。原因は2つに分かれ、片方は今も仕様として残り、もう片方はv2.0.64で修正されました。
Claude Codeが.envを読み込むとテストが壊れる
GitHub issue #401は2025年3月8日に報告されました。報告者はLaravelのテストスイートを実行するたびに、開発用データベースの内容が消える現象に悩まされていました。原因を調べると、Claude Codeが起動時にプロジェクトの.envファイルをBashツールのプロセス環境変数として読み込んでいたと判明します。phpunit.xmlで指定したテスト用データベースの設定は、.env由来のプロセス環境変数に上書きされていました。
同様の報告は半年近く途切れませんでした。2025年7月には複数の開発者がv1.0.58でも.env.developmentが混入すると確認し、2025年10月17日には利用者のMichMich氏が「phpunitのテストを実行するたびにデータベースが消される」と改めて報告しています。
影響はWebフレームワークだけに留まりませんでした。2025年10月7日、bpedman氏はAmazon Bedrock経由でClaude Codeを使う環境で、.envに書いていたAWS_PROFILEがsettings.jsonで指定した値を上書きし、モデルにアクセスできなくなったと報告しています。チームで共有しているBedrock用プロファイルが、開発者個人の.envの値に静かに差し替わっていた格好です。データ破壊と認証失敗という、性質の違う2種類の実害が同じ根から出ていたことになります。
原因は2つある — 仕様のシェル継承と意図しない自動読み込み
.envが混ざる2つの経路
シェル起動ファイルの継承
~/.zshrcや~/.bashrcでexportした変数が、Bashツールの環境に引き継がれます。クラウド認証情報を渡す目的の設計です。
BunによるENV自動読み込み
ネイティブインストーラーのランタイムが、作業ディレクトリの.env系ファイルを起動時にプロセス環境へ入れていました。
Anthropicのbcherny氏は報告直後、この挙動の一部を仕様として説明しました。Claude Codeは起動時に~/.zshrcや~/.bashrcをソースし、AWSやGCPの認証情報を含む環境変数をBashツールに引き継ぎます。ユーザーの代わりにクラウドサービスを操作させるための設計で、この部分は今も変わっていません(シェルごとの読み込みタイミングは別記事にまとめています)。
問題はもう一つ別にありました。2025年8月8日、rsanheim氏がネイティブインストーラー内部で使われるBunランタイムの挙動を突き止めます。BunはNODE_ENVが未設定か空文字のときdevelopmentとみなし、.envのあとに.env.developmentを読み込みます。NODE_ENV=productionのときは.envのあとに.env.productionを読み込みます。厄介なのはここからで、Bunが読み込んだ値はプロセスの環境変数になるため、アプリ側が後から.env.testを読み込んでも、既存のプロセス環境変数を上書きしない読み込み方なら値は変わりません。結果として.env.testに書いた値ではなく、先に読み込まれた.env.developmentの値が実行時に使われていました。
VS Code拡張では挙動が違いました。MichMich氏は2025年10月24日、CLIでは.envを読まないのに拡張では読むと報告しています。拡張がネイティブバイナリを同梱していたため、同じBunの挙動が拡張側で起きていたという指摘がRobinDaugherty氏から出ています。
Ruby on Railsのdotenv-railsを使うチームも同じ経路の影響を受けました。LandonSchropp氏によれば、.env.developmentにRAILS_ENV=developmentを書く構成では、Bunがこの値を読み込んだ時点でBundlerがtestグループのgemを読み込まなくなり、Claude経由のテスト実行そのものが失敗していました。この自動読み込みは常に起きるとは限らず、Dambre氏は「数日前から急に起こるようになった」と報告しており、発生条件の分かりにくさが原因調査を長引かせた一因になっています。
この読み込みはClaude Codeのプロセス起動と同時に走るため、hooksやCLAUDE.mdの指示より先に完了していました。joenyambura氏は2025年8月7日、最速のSessionStart hookでもこの読み込みには間に合わないと報告しています。hooksという後付けの制御点そのものが、起動シーケンス上そもそも間に合わない位置にあったわけです。
v2.0.64で.envの自動読み込みが修正された
issue #401から修正までの流れ
- 2025-03-08issue #401が起票される
bcherny氏がシェル継承は仕様と説明しました。
- 2025-07-23v1.0.58でも混入を再確認
.env.developmentの混入が複数人から報告されました。 - 2025-08-08Bunの挙動が原因と特定
rsanheim氏が
NODE_ENV依存の読み込みを突き止めました。 - 2025-10-06v2.0.xでも継続
yuheitomi氏がv2.0.8、bpedman氏がv2.0.1での発生を報告し、10月17日にはMichMich氏がデータベース破壊を再報告しました。
- 2025-12-10v2.0.64で修正
changelogに「Fixed auto-loading .env when using native installer」と載りました。
修正はv2.0.64で入りました。issue #401が最終的にクローズされたのは2026年4月17日で、修正リリースの4か月ほど後です。issueのステータスだけでは、いつ直ったかを判断できません。修正の中身については、Bun開発者のJarred-Sumner氏が、Bun 1.3.3で単一実行ファイルの.env自動読み込みを無効にする--no-compile-autoload-dotenvが使えるようになったと説明しています。changelogのv2.1.289(2026年10月3日)までに、.envの自動読み込みが再発したという記載はありません。
修正前の時期には、インストール方法を変える回避策も報告されていました。2025年10月6日、yuheitomi氏はネイティブインストーラーからレガシーなnpmインストーラーに切り替えると問題が起きなくなったと報告しています。当時のnpm版はBunを経由しない配布形態だったため、結果的にこの不具合を避けられていました。
この回避策は今では成立しません。現在のnpmパッケージは、プラットフォーム別の依存パッケージ経由でネイティブインストーラーと同じバイナリを入れる仕組みです。「npmの旧インストーラーに戻す」手順は、今のインストール方式には当てはまりません。
今も使える3つの対策層
根本原因は塞がれましたが、.envの値がBashの実行環境に混ざる経路はまだ残っています。プロジェクトのMakefileやシェルスクリプトがsource .envを呼ぶ構成では、同じ現象が別の形で起こり得ます。守りたい値の性質に応じて、3つの対策を使い分けます。
| 対策 | 防ぐ範囲 | 必要バージョン | 効かないもの |
|---|---|---|---|
permissions.denyのRead(./.env) | 防ぐ範囲Read/Editツールとcat・head・tail・sedなど認識済みのBashコマンドがファイル内容を表示すること | 必要バージョンバージョン不問 | 効かないもの既にプロセス環境へ読み込まれた値をenvで確認する操作、任意のサブプロセス。grep -rはv2.1.259以降、拒否ではなく確認が出る |
sandbox.credentials.envVars(deny/mask) | 防ぐ範囲指定した変数名をサンドボックス化されたBashコマンドの環境から除去または置換 | 必要バージョンsandbox.credentialsはv2.1.187で追加 | 効かないもの列挙していない変数名、サンドボックスを有効にしていないセッション |
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB | 防ぐ範囲変数名または値の形が認証情報に見えるものを、Bash・hooks・MCPの子プロセス全体から除去 | 必要バージョン既定は無効 | 効かないものRAILS_ENVなど認証情報に見えないアプリ設定値、GITHUB_TOKENなどの例外 |
そもそも今回のBun起因の問題は、Read拒否ルールでは止められませんでした。値はプロセス起動と同時にBashの環境変数として届いており、Read/Editツールを経由しないため対象外だったからです。確実に止めるには、permissions.denyとサンドボックスのcredentials.envVarsを組み合わせます。設定例は次の通りです。
{
"permissions": {
"deny": ["Read(./.env)", "Read(./.env.*)"]
},
"sandbox": {
"enabled": true,
"credentials": {
"envVars": [
{ "name": "DATABASE_URL", "mode": "deny" },
{ "name": "RAILS_ENV", "mode": "deny" }
]
}
}
}denyエントリは、読み込まれた全スコープの設定が合算されます。.claude/settings.jsonに書いてリポジトリへコミットすればチーム全員に同じ制約を配れますし、~/.claude/settings.jsonなら自分専用です。どのスコープからも、他のスコープが足したdenyを外すことはできません。
注意点が3つあります。
credentials.envVarsはサンドボックス機能の一部なので、sandbox.enabledをtrueにしていないセッションには効きません- 組み込みの拒否リストはなく、守りたい変数名は自分で列挙します
maskモードはユーザー設定・管理設定・--settingsフラグでだけ有効になり、リポジトリの.claude/settings.jsonに書いても無視されます
maskは許可したホストへの通信でだけ実値に差し替わる仕組みです。GH_TOKENのように送信先をapi.github.comなどに絞れるトークンに向きます。プロジェクトの.envにあるローカル専用の接続文字列は送信先を絞りにくいため、denyで丸ごと外すほうが実用的です(マスキングの詳細設定は別記事で扱っています)。
CLAUDE_CODE_SUBPROCESS_ENV_SCRUBは、Bash・hooks・MCPのstdioサーバーのすべてに効きます。除去の判断は変数名と値の形で決まるため、.envの中身の扱いは変数ごとに分かれます。
.envの変数 | スクラブの結果 |
|---|---|
パスワードを含むDATABASE_URL | スクラブの結果値が認証情報の形なので除去される |
DB_PASSWORD・NPM_TOKEN | スクラブの結果名前が認証情報らしいので除去される |
RAILS_ENV・NODE_ENV | スクラブの結果認証情報に見えないので残る |
GITHUB_TOKEN・GH_TOKEN | スクラブの結果ghが動くよう意図的に残される |
issue #401で問題になったRAILS_ENVの混入は、スクラブでは防げません。GitHubトークンを子プロセスから外したいときは、サンドボックスのcredentialsでdenyを足します。ただしこのdenyが効くのはサンドボックス化されたBashコマンドだけで、hooksやMCPサーバーは対象外です。遮断範囲は専用記事で詳しく扱っています。
なぜ.env.testの値が負けるのか — 環境変数の優先順位を再現する
後から.env.testを読み込んでも値が変わらなかった理由は、既にプロセス環境にある変数を上書きしない読み込み方にあります。Node.js 20.18のprocess.loadEnvFileがその一例です。
printf 'DB_DATABASE=dev_db\n' > .env.development
printf 'DB_DATABASE=test_db\n' > .env.test
DB_DATABASE=dev_db node -e "process.loadEnvFile('.env.test'); console.log(process.env.DB_DATABASE)"
# dev_db
node -e "process.loadEnvFile('.env.test'); console.log(process.env.DB_DATABASE)"
# test_db1行目の実行では、先に.env.development相当の値がプロセス環境に入っているため、.env.testが負けてdev_dbのままです。Bunが起動時に.env.developmentを入れていた状況は、この1行目と同じ形でした。
よくあるつまずき
CLAUDE.mdに「.envを読むな」と書くだけの対策が、コミュニティで一時期広まりました。2025年9月にfatihaziz氏が公開した手順で、実際に拒否メッセージを再現できます。この手順が効くのは、Claudeがモデルとして指示文を読み、自発的にファイルへのアクセスを控えるからです。権限システムのように強制的にブロックしているわけではありません。本人も「長い会話では従わないことがある」と注記しており、プロンプト上の指示なのでBashの実行環境そのものは制御できません。恒常的に守りたい値がある場合は、前段で挙げたpermissions.denyかサンドボックスの設定に置き換えるほうが確実です。
Laravelのphpunit向けには、joenyambura氏が2025年8月に示した回避策が今も有効です。phpunit.xmlの<env>を<server>に書き換えると、Bashの環境変数より<server>の値が優先されるため、Claude経由のテスト実行でもSQLiteのインメモリDBを使えます。
<php>
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
</php>混入を疑うときの切り分けは、次の3段階で進められます。
混入の切り分け手順
- 1
バージョンを確かめる
claude --versionでv2.0.64より前でないかを見ます。CIやコンテナの固定バージョンは見落としやすい箇所です。 - 2
Bashツール側の環境を見る
テストやマイグレーションの直前に、疑わしい変数名を指定してプロセス環境を確認します。
- 3
出どころを分ける
~/.zshrcなどのexportなら仕様どおりの継承、プロジェクトのスクリプトのsource .envなら自前の読み込みです。
claude --version
env | grep -E 'DATABASE_URL|RAILS_ENV|NODE_ENV'まとめ
v2.0.64以降の環境では、.envがBashへ入る原因はシェル起動ファイルの継承か、プロジェクト側のsource .envのどちらかです。破壊的な操作を止めたいなら、Readの拒否ルールでは足りず、サンドボックスのcredentials.envVarsで変数名を列挙する必要があります。