Claude Codeでdirenvの環境変数を安全に渡す設定
direnvの.envrcはClaude CodeのBashツールに自動では効きません。SessionStart・CwdChanged・FileChangedの3フックで渡す設定と、direnv allowを人に残す権限設定をまとめます。
direnvは、ディレクトリに入るとシェルへ環境変数を読み込み、出ると外す道具です。ところがClaude CodeのBashツールは、direnvのこの切り替えを自動では拾いません。.envrcを置いただけでは、Claudeが実行するコマンドに変数は届かないということです。
届けるには、direnv export bashの出力をCLAUDE_ENV_FILEへ書くフックを登録します。あわせて、direnv allowをClaudeに実行させない権限設定を入れると、読み込む .envrc を人が承認する形に保てます。
なぜ.envrcがBashツールに効かないのか
direnvは、シェルのプロンプトが表示される直前に.envrcを探します。見つかって許可済みなら、bashのサブシェルで読み込み、exportされた変数の差分を現在のシェルに反映します。つまりdirenvの仕組みは「プロンプトの前に動くシェルフック」に依存しています。
Claude Codeのドキュメントは、この前提が崩れる点をはっきり書いています。direnvのようなツールはシェルでは自動で動くが、ClaudeのBashツールは変更を自力では拾わない、という説明です。Bashツールのコマンドは、シェルのプロンプトを経由せずに実行されます。
そこで使うのがCLAUDE_ENV_FILEです。Claude Codeはこのファイルの中身を、各Bashコマンドの前に同じシェルプロセスで実行します。ファイルに書いたexportは、直後のコマンドから見えます。この仕組みの全体像はCLAUDE_ENV_FILEでvenvとcondaを引き継ぐ記事に譲り、ここではdirenvに絞ります。
3つのフックで.envrcを渡す
ドキュメントの例は、SessionStartとCwdChangedに同じコマンドを登録する形です。~/.claude/settings.jsonに次を書きます。
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
}
]
}
],
"CwdChanged": [
{
"hooks": [
{
"type": "command",
"command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
}
]
}
]
}
}あとは.envrcのあるディレクトリで、人が一度direnv allowを実行します。direnvのマニュアルによると、許可していない.envrcは読み込まれません。
direnvを使い始めるまでの流れ
- 1
direnvを入れてシェルにフックする
通常のシェル利用のための手順です。direnvの公式ページに、bash・zsh・fishなどのフック設定が載っています。
- 2
プロジェクトに.envrcを置く
export FOO=fooのように、bashとして書きます。 - 3
自分のターミナルでdirenv allowを実行する
.envrcの内容を確認してから許可します。 - 4
settings.jsonにフックを登録する
上のJSONを
~/.claude/settings.jsonに足します。 - 5
Claude Codeを起動し直す
SessionStartが起動時の変数を、CwdChangedが移動後の変数を書き出します。
3つのイベントの役割
どのフックがいつ書くか
SessionStart
起動したディレクトリの
.envrcを読みます。ここが無いと、最初のコマンドには変数がありません。CwdChanged
Claudeが
cdしたときに再読み込みします。書かれた内容は、次のCwdChangedでClaude Codeが消します。FileChanged
.envrcを編集した直後に読み直します。matcherに.envrc|.envを書きます。
FileChangedを足すなら、次のブロックをhooksに加えます。matcherは正規表現ではなく、|で区切った作業ディレクトリ直下のファイル名そのものとして登録されます。
"FileChanged": [
{
"matcher": ".envrc|.env",
"hooks": [
{
"type": "command",
"command": "direnv export bash > \"$CLAUDE_ENV_FILE\""
}
]
}
]Claudeが.envrcにキーを足した場合も、人がエディタで直した場合も、ファイルシステム側の変化として検知されます。ただし、読み込みの許可は人が握る前提です。.envrcを書き換えたあとに変数が更新されないときは、自分のターミナルで内容を見直してからdirenv allowを実行します。
動いているかを確かめる
フックは設定したつもりでも、変数が空のままで気づきにくい部分です。.envrcにexport FOO=fooだけを書いたプロジェクトで、Claudeに次のコマンドを実行させます。
echo "FOO=${FOO-nope}"FOO=fooと返れば届いています。FOO=nopeなら、次の順で切り分けます。
変数が届かないときの切り分け
- Q自分のターミナルで同じディレクトリに入ると FOO は見えるか?答えがいいえのとき答えがはい
direnv自体の問題です。
direnv allowを実行し、シェルのフック設定を見直します。 - QClaude Codeを起動し直したか?答えがいいえのとき答えがはい
SessionStartは起動時に走るフックです。設定を足した直後は起動し直します。
フックの登録を疑う
settings.jsonのhooksに、SessionStartとCwdChangedのコマンドが書かれているかを見ます。claude --debugのログも手がかりになります。
direnv allowをClaudeに実行させない
direnvのallowは、安全のための関門です。マニュアルは、新しいファイルを自動では読み込まない理由を、pullしたgitリポジトリや展開したアーカイブに悪意のある.envrcが入っていても、cdしただけで実行されるのを防ぐためだと説明しています。
関門は、承認する主体が人であるときに意味を持ちます。Claudeは.envrcを編集でき、Bashでdirenv allowも実行できるので、権限設定で後者を止めておきます。
{
"permissions": {
"deny": [
"Bash(direnv allow *)",
"Bash(direnv edit *)"
]
}
}direnv editは、エディタを開いたうえで読み込みを許可するコマンドなので、並べて止めます。
ただしBashのdenyは、コマンドの書かれ方に頼るルールです。権限のドキュメントは、/bin/rmやbash -c '...'のような別の書き方は、Bash(rm *)では止まらないと注意しています。direnv allowも同じで、このdenyは「いつもの形」を止める柵に当たります。完全な境界を求めるなら、.envrcの内容を人が見る運用を前提にします。
また、source_envが読む別の.envrcは、direnvのセキュリティの仕組みで検査されないとstdlibのマニュアルにあります。許可済みの.envrcから呼ばれるファイルは、Claudeが書き換えられる場所に置かないほうが無難です。
direnv execを使うときの権限ルール
フックを使わず、コマンドごとにdirenv exec . npm testと書く方法もあります。この場合、権限ルールの書き方に注意が要ります。direnv execは、Claude Codeが外側を剥がして中身を判定するラッパーの一覧に入っていません。Bash(direnv exec *)と書くと、execの後ろの何でも許すルールになります。許可したいコマンドごとに、Bash(direnv exec . npm test)のようにラッパーと中身をセットで書きます。
秘密を.envrcの外へ出す
.envrcはただのbashスクリプトで、プロジェクトの中に置かれます。そこへAPIキーを直接書くと、Readツールからも見えます。値を別の場所に置き、.envrcは読み込むだけにする書き方があります。
# .envrc
dotenv_if_exists ~/.secrets/my-project.env
export APP_ENV=developmentdotenv_if_existsはdirenvのstdlibの関数で、.env形式のファイルが存在するときだけ読み込みます。秘密はホーム配下に置いたファイルに入り、リポジトリにも作業ディレクトリにも載りません。プロジェクト直下の.envrcと.envを読ませたくない場合は、Readのdenyで塞ぎます。書き方と効く範囲は.envを読ませないRead denyの解説にあります。
最後に、変数の値は隠れない点を押さえておきます。CLAUDE_ENV_FILEに書いたexportは、Bashコマンドから見えるものとして実行されます。Claudeがechoやenvを実行すれば、値は出力に現れます。ここで言う安全は、承認を人に残す、秘密をリポジトリから離す、不要な変数を渡さない、という範囲のことです。値そのものをClaudeから隠す仕組みではありません。
ハマりやすいところ
落とし穴
別のフックが書いた変数が消える
>で書くと、CLAUDE_ENV_FILEは毎回上書きされます。ほかのフックも同じファイルに書く構成では、>>で追記しないと先に書かれた行が消えます。direnv export bashが環境全体を出し直す前提なら>でよく、混在させるなら>>にします。
- PowerShellでは渡らない場合がある: v2.1.296以降、PowerShellのコマンドも
CLAUDE_ENV_FILEの変数を受け取ります。ただし、ファイルの全行が単純な代入でなければなりません。direnv export bashは$'...'形式の文字列を出すことがあり、その場合PowerShellには変数が1つも渡りません。Bashのコマンドには渡ります - 対象はbashとzshだけ:
CLAUDE_CODE_SHELLが受け付けるのはbashかzshのパスです。fishなど他のシェルは使えません - aliasと関数は渡らない: direnvの変更はサブプロセスで計算され、環境変数の差分だけが戻ります。
.envrcで定義したaliasや関数は、Bashツールに届きません - .envrcの
.env読み込みと混同しない: direnvは、.envrcに加えて.envも読む設定があります。.envを取り込むと、そこの値もClaudeのコマンドへ渡ります。意図せず読み込まれる問題は.envが自動で読み込まれていた問題の記事にあります
フック自体の入出力はFileChangedフックのチュートリアルが詳しく扱っています。
まとめ
.envrcをClaudeのBashツールに効かせる条件は、SessionStart・CwdChanged・必要ならFileChangedでdirenv export bashをCLAUDE_ENV_FILEへ書くことです。そこまで整えたら、残る設計の軸は承認です。direnv allowは人が実行し、ClaudeにはBash(direnv allow *)とBash(direnv edit *)のdenyで止めます。秘密は.envrcに直書きせず、dotenv_if_existsで外のファイルに逃がします。
変数の値がBashコマンドから見える点は、direnvを使っても変わりません。渡す変数は、そのプロジェクトのコマンドに本当に必要なものに絞ります。